From b686cf2e0e7e6b55f3bf62e381386e33effc9f86 Mon Sep 17 00:00:00 2001 From: Scott Date: Tue, 3 Mar 2020 13:32:14 +1100 Subject: [PATCH] Feature: Websocket order handling (#446) * Initial changes, removing exchange name as an arg and puts it in the pointer struct. Adds case to ws routines * Adds CancelAllOrders func, adds GetByExchangeAndID. Adds modify handler in routines.go * initial poor attempts to have bitmex work with new datahandler handlers. fixes ordersides * bitmex Completes new order * Better bitmex handling, but not complete. Begins a gargantuan task of unifying order data structs. Sometimes an order update will contain lot's of information, so its best to be able to update all fields of our orders, rather than just an arbitrary subset. As a result, everything will be broken for the foreseeable future :glitch_crab: * Removes old order handler which did nothing. Updates order properties for everything everywhere - now consistent. Changes order status. Adds asset type and wallet address to all order types * Adds order updater to update only relevant fields since the object is generic, we don't know what fields are passed from what exchanges. Adds "lastupdated" field to order.Detail. Expands order cancellation for engine orders. * Ensures that new orders are added to the ordermanager's order store. Saaa many comments. Internalises orderStore get func. Adds internalOrderID to orderdetail and adds websocket support for it * Fixes a cancelAllOrders oopsie doopsie * Adds potential func to update orderdetails from an orderdetail struct. Unsure if will keep. * Begins btcmarkets implementation. Expands order "stringToOrder" funcs to allow for some more flexible string coversions. Removes order.Submit via websocket as it would cause unlimited order place issues :D * Finishes btc markets without testing * Adds untested ws auth func to btse * Finises btse, fixes btcmarkets bug * Adds coinbasepro support * Fixes a few more fields in coinbase pro and readds the extra subs * Begins work on coinbene. Plus theyve added a new ws connection yeee * Wasted a bunch of time adding support to an additional websocket that isn't needed ;_; Fixed a bug in coinbasepro. Fully kitted out coinbene support. Updates order types with all fields * Removes extra websocket connection ;_; * Finishes gemini. Fixes order side unknown * Adds okgroup support. Moves byte reading to another function to allow for unit testing. Updates routines to use pointers. Updates date update handling for order details * Finishes order data for okgroup websocket, but starts the STRANGE process of converting all other websocket endpoints to be a little less silly * Cleans up okroup websocket implementation. Fixes bug in Gemini * Adds poloniex support. Updates ws order handling * new bitmex support. Adds some tests now that its all in its own func. Fixes poloniex bug * Begins work on authenticated binance websocket * Attempts to track user data via binance websocket * Maybe finishes Binance websocket support * Begins adding test coverage to orders.go. Updates names of script properties to match updated * Begins an experiment with code coverage. Fixes more rebase issues * Completes orders coverage. Botches a few other things though. Fixes more scripting stuff * All tests in engine package pass * Adds some loevely routine tests * Moves ordermanager to test Bot ordermanager Adds lovely routine tests to ensure things that get sent to be handled the data handler are handled by the data handler by handling them * Replaces "wsHandleData" with "wsReadData" as that's what its going to do now. * Splits all wsHandleData into wsReadData and wsHandleData to allow for easy testing via sending []byte json examples to test proper functionality. Breaks so many tests * Fixes majority of test issues. But data races which are tough on the engine package * "Fixes" test by removing shutdown test. It interferes with too many things. Requires some thought * Tests all the binance websocket points * Adds better bitfinex websocket support. * Adds testing for bitfinex, bitstamp and btcmarkets. Fixes websocket bugs encountered * Adds BTSE ws tests. Fixes bugs in ws * Adds coinbase pro tests. Fixes any issues * Coinbene tests * Starts to handle coinut. Runs into a problem conceptually regarding websocket roundtrip and orders. Both events need to happen without impacting eachother/racing * Addresses a data race issue regarding websocket and bot order management submission - order submission locks at an earlier point to prevent routines.go from creating an order before order submission creates it. Updates rpcserver to use order management bot to submit orders. * Finishes the hectic coinut testing * Adds tests for gateio * Fixes rebase issues. Updates tests to work without being overloaded * Begins testing of gemini. fixes up minor issues * ginishes gemini tests and fixes * Adds hitbtc tests. Fixes all the many issues with hitbtc websocket * Adds remaining tests. Increases default test channel limit again * Begins work towards huobi tests * Finishes huobi tests * Fixed all mythical rebase adventures * Begins kraken transformation * Finishes kraken. Fixes coinbene leverage now that its changed * Begins okgroup testing * Adds okgroup ws tests * Does some poloniex * Fixes basic curreny issue by extracting to func * Begins redesign of poloniex websocket datahandling. Completes authenticated handling, now onto unauth * Finishes poloniex revision * Finishes ZB additions * Fixes data races * Fixes rebase issues. Fixes bad kraken logic * Fixes after reviewing code * lint everywhere * Fixes lingering lints * lint * Adds test coverage to order detail and modify updating * Fixes linting * Fixes huge int, fixes date tests * Adds GetByExchange, adds test for it. Protects fakepass echange. Renames DisplayQty to DisplayQuantity. Removes verbose. Adds some websocket properties. Updates bitmex asset type in test * Addresses timestamps, type abbreviations, verbosity. Expands binance kline switch cases. Updates some websocket capabilities. * Adds coverage to the stringToOrderType/Status functions introduced in PR * Minor fixes addressing some time, error text and use of StringDataCompareInsensitive * Introduces shiny new system which checks if there is an awaiting ID, if found, processes via wrapper method, else, goes through wsHandleData method. Removes weird locking system from wrapper/websocket data race. Updates bitfinex to properly handle websocket order requests and notifications * Moves fakePassingExchange to test_helper. Fixes some order side implementations for trades. Botches a new error type * Adds new error type to track and handle order classification errors separately * Fully fleshes out ClassificationError for all instances of status conversion. Even in order trades and some wrapper functions * Introduces common.SimpleTimeFormat for "2006-01-02 15:04:05". Fixes binance and bitfinex issues with auth endpoint use, map casting. Expands more order.ClassificationError usage. Fixes some more generic websocket response errors * Future proofs order updating by utilising asset types. Expands testing to accomodate. Adds shiny new time type. Expands wrapper websocket functionality definitions * minty linty * Broken end of day code addressing basic nits on comments, returns and currency conversion * Adds testing to btcmarkets websocket. Also updates websocket orderbook to use update instead * Fixes fun rebase fun fun so fun * Addresses minor nits regarding changed interface and comments * Creates new function `GetRequestFormattedPairAndAssetType` to retrieve a currency pair and asset type based on a string. It will iterate over enabled pairs and compare them to formatted pairs and then return that pair if found. * Fixes test * Adds a single line to the end of the file, because that would be really bad if it wasn't there * Updates fakepassexchange to not use params, updates test params, uses fatal in some tests where its important, updates order manager to have a rwmutex, removes some returns, improves ws key test for binance, updates properties to reflect their actual values, adds some more websocket properties * Addresses binance switch linting * Updates leverage property to int64 * Fixes what was broken --- cmd/exchange_wrapper_coverage/main.go | 12 +- cmd/exchange_wrapper_issues/main.go | 46 +- cmd/gctcli/commands.go | 27 +- common/common.go | 3 + database/repository/audit/audit.go | 3 - engine/events_test.go | 28 +- engine/exchange_test.go | 72 +- engine/fake_exchange_test.go | 192 +++ engine/helpers_test.go | 42 +- engine/orders.go | 288 ++-- engine/orders_test.go | 379 +++++ engine/orders_types.go | 6 +- engine/restful_server_test.go | 31 +- engine/routines.go | 212 ++- engine/routines_test.go | 127 ++ engine/rpcserver.go | 45 +- engine/syncer.go | 2 +- engine/withdraw_test.go | 31 +- exchanges/alphapoint/alphapoint_test.go | 22 +- exchanges/alphapoint/alphapoint_wrapper.go | 36 +- exchanges/binance/binance.go | 71 +- exchanges/binance/binance_live_test.go | 1 + exchanges/binance/binance_mock_test.go | 2 +- exchanges/binance/binance_test.go | 302 +++- exchanges/binance/binance_types.go | 134 +- exchanges/binance/binance_websocket.go | 431 ++++-- exchanges/binance/binance_wrapper.go | 78 +- exchanges/bitfinex/bitfinex_test.go | 109 +- exchanges/bitfinex/bitfinex_types.go | 42 +- exchanges/bitfinex/bitfinex_websocket.go | 1215 +++++++++-------- exchanges/bitfinex/bitfinex_wrapper.go | 52 +- exchanges/bitflyer/bitflyer_test.go | 22 +- exchanges/bithumb/bithumb_test.go | 34 +- exchanges/bithumb/bithumb_wrapper.go | 38 +- exchanges/bitmex/bitmex_parameters.go | 54 +- exchanges/bitmex/bitmex_test.go | 267 +++- exchanges/bitmex/bitmex_types.go | 616 ++++----- exchanges/bitmex/bitmex_websocket.go | 532 +++++--- exchanges/bitmex/bitmex_websocket_types.go | 165 ++- exchanges/bitmex/bitmex_wrapper.go | 68 +- exchanges/bitstamp/bitstamp_live_test.go | 2 + exchanges/bitstamp/bitstamp_mock_test.go | 3 +- exchanges/bitstamp/bitstamp_test.go | 86 +- exchanges/bitstamp/bitstamp_types.go | 2 +- exchanges/bitstamp/bitstamp_websocket.go | 137 +- exchanges/bitstamp/bitstamp_wrapper.go | 46 +- exchanges/bittrex/bittrex_test.go | 26 +- exchanges/bittrex/bittrex_wrapper.go | 34 +- exchanges/btcmarkets/btcmarkets.go | 2 +- exchanges/btcmarkets/btcmarkets_test.go | 221 ++- exchanges/btcmarkets/btcmarkets_types.go | 11 +- exchanges/btcmarkets/btcmarkets_websocket.go | 357 +++-- exchanges/btcmarkets/btcmarkets_wrapper.go | 84 +- exchanges/btse/btse_test.go | 77 +- exchanges/btse/btse_types.go | 20 + exchanges/btse/btse_websocket.go | 305 +++-- exchanges/btse/btse_wrapper.go | 44 +- exchanges/coinbasepro/coinbasepro_test.go | 339 ++++- exchanges/coinbasepro/coinbasepro_types.go | 142 +- .../coinbasepro/coinbasepro_websocket.go | 315 +++-- exchanges/coinbasepro/coinbasepro_wrapper.go | 44 +- exchanges/coinbene/coinbene_test.go | 234 +++- exchanges/coinbene/coinbene_types.go | 92 +- exchanges/coinbene/coinbene_websocket.go | 522 +++---- exchanges/coinbene/coinbene_wrapper.go | 75 +- exchanges/coinut/coinut.go | 2 +- exchanges/coinut/coinut_test.go | 525 ++++++- exchanges/coinut/coinut_types.go | 329 ++--- exchanges/coinut/coinut_websocket.go | 451 +++--- exchanges/coinut/coinut_wrapper.go | 130 +- exchanges/exchange.go | 27 +- exchanges/exchange_test.go | 36 + exchanges/exchange_types.go | 2 +- exchanges/exmo/exmo_test.go | 24 +- exchanges/exmo/exmo_wrapper.go | 46 +- exchanges/gateio/gateio_test.go | 199 ++- exchanges/gateio/gateio_types.go | 12 + exchanges/gateio/gateio_websocket.go | 512 ++++--- exchanges/gateio/gateio_wrapper.go | 56 +- exchanges/gemini/gemini_live_test.go | 2 + exchanges/gemini/gemini_mock_test.go | 3 +- exchanges/gemini/gemini_test.go | 518 ++++++- exchanges/gemini/gemini_types.go | 162 +-- exchanges/gemini/gemini_websocket.go | 308 +++-- exchanges/gemini/gemini_wrapper.go | 51 +- exchanges/hitbtc/hitbtc_test.go | 434 +++++- exchanges/hitbtc/hitbtc_types.go | 100 +- exchanges/hitbtc/hitbtc_websocket.go | 316 +++-- exchanges/hitbtc/hitbtc_wrapper.go | 56 +- exchanges/huobi/huobi_test.go | 405 +++++- exchanges/huobi/huobi_types.go | 68 +- exchanges/huobi/huobi_websocket.go | 256 ++-- exchanges/huobi/huobi_wrapper.go | 139 +- exchanges/itbit/itbit_test.go | 22 +- exchanges/itbit/itbit_wrapper.go | 26 +- exchanges/kraken/kraken_test.go | 652 ++++++++- exchanges/kraken/kraken_types.go | 80 +- exchanges/kraken/kraken_websocket.go | 587 ++++---- exchanges/kraken/kraken_wrapper.go | 48 +- exchanges/lakebtc/lakebtc_test.go | 22 +- exchanges/lakebtc/lakebtc_websocket.go | 11 +- exchanges/lakebtc/lakebtc_wrapper.go | 42 +- exchanges/lbank/lbank_test.go | 16 +- exchanges/lbank/lbank_wrapper.go | 48 +- exchanges/localbitcoins/localbitcoins_test.go | 22 +- .../localbitcoins/localbitcoins_wrapper.go | 38 +- exchanges/mock/server.go | 2 +- exchanges/okcoin/okcoin_test.go | 66 +- exchanges/okcoin/okcoin_wrapper.go | 3 + exchanges/okex/okex_test.go | 370 ++++- exchanges/okex/okex_wrapper.go | 3 + exchanges/okgroup/okgroup_types.go | 241 +--- exchanges/okgroup/okgroup_websocket.go | 450 +++--- exchanges/okgroup/okgroup_wrapper.go | 46 +- exchanges/order/order_test.go | 486 ++++++- exchanges/order/order_types.go | 300 ++-- exchanges/order/orders.go | 419 ++++-- exchanges/order/orders_test.go | 37 - exchanges/poloniex/poloniex.go | 2 - exchanges/poloniex/poloniex_live_test.go | 2 + exchanges/poloniex/poloniex_mock_test.go | 3 +- exchanges/poloniex/poloniex_test.go | 127 +- exchanges/poloniex/poloniex_types.go | 54 +- exchanges/poloniex/poloniex_websocket.go | 532 ++++---- exchanges/poloniex/poloniex_wrapper.go | 52 +- exchanges/request/request.go | 5 +- .../sharedtestvalues/sharedtestvalues.go | 2 +- exchanges/websocket/wshandler/wshandler.go | 21 +- .../websocket/wshandler/wshandler_test.go | 32 +- .../websocket/wshandler/wshandler_types.go | 12 +- exchanges/yobit/yobit_test.go | 26 +- exchanges/yobit/yobit_wrapper.go | 46 +- exchanges/zb/zb_test.go | 274 +++- exchanges/zb/zb_websocket.go | 290 ++-- exchanges/zb/zb_wrapper.go | 66 +- gctscript/modules/gct/exchange.go | 22 +- gctscript/modules/wrapper_types.go | 2 +- gctscript/wrappers/gct/exchange/exchange.go | 14 +- .../wrappers/gct/exchange/exchange_test.go | 7 +- gctscript/wrappers/validator/validator.go | 19 +- .../wrappers/validator/validator_test.go | 9 +- testdata/http_mock/binance/binance.json | 28 + 142 files changed, 13867 insertions(+), 6043 deletions(-) create mode 100644 engine/fake_exchange_test.go create mode 100644 engine/orders_test.go create mode 100644 engine/routines_test.go delete mode 100644 exchanges/order/orders_test.go diff --git a/cmd/exchange_wrapper_coverage/main.go b/cmd/exchange_wrapper_coverage/main.go index 070b3c0c..52be1860 100644 --- a/cmd/exchange_wrapper_coverage/main.go +++ b/cmd/exchange_wrapper_coverage/main.go @@ -128,12 +128,12 @@ func testWrappers(e exchange.IBotExchange) []string { } s := &order.Submit{ - Pair: p, - OrderSide: order.Buy, - OrderType: order.Limit, - Amount: 1000000, - Price: 10000000000, - ClientID: "meow", + Pair: p, + Side: order.Buy, + Type: order.Limit, + Amount: 1000000, + Price: 10000000000, + ClientID: "meow", } _, err = e.SubmitOrder(s) if err == common.ErrNotYetImplemented { diff --git a/cmd/exchange_wrapper_issues/main.go b/cmd/exchange_wrapper_issues/main.go index 60504566..b3da3351 100644 --- a/cmd/exchange_wrapper_issues/main.go +++ b/cmd/exchange_wrapper_issues/main.go @@ -262,8 +262,8 @@ func parseOrderType(orderType string) order.Type { return order.Stop case order.TrailingStop.String(): return order.TrailingStop - case order.Unknown.String(): - return order.Unknown + case order.UnknownType.String(): + return order.UnknownType default: log.Printf("OrderType '%v' not recognised, defaulting to LIMIT", orderTypeOverride) @@ -462,12 +462,12 @@ func testWrappers(e exchange.IBotExchange, base *exchange.Base, config *Config) }) s := &order.Submit{ - Pair: p, - OrderSide: testOrderSide, - OrderType: testOrderType, - Amount: config.OrderSubmission.Amount, - Price: config.OrderSubmission.Price, - ClientID: config.OrderSubmission.OrderID, + Pair: p, + Side: testOrderSide, + Type: testOrderType, + Amount: config.OrderSubmission.Amount, + Price: config.OrderSubmission.Price, + ClientID: config.OrderSubmission.OrderID, } var r11 order.SubmitResponse r11, err = e.SubmitOrder(s) @@ -484,12 +484,12 @@ func testWrappers(e exchange.IBotExchange, base *exchange.Base, config *Config) }) modifyRequest := order.Modify{ - OrderID: config.OrderSubmission.OrderID, - Type: testOrderType, - Side: testOrderSide, - CurrencyPair: p, - Price: config.OrderSubmission.Price, - Amount: config.OrderSubmission.Amount, + ID: config.OrderSubmission.OrderID, + Type: testOrderType, + Side: testOrderSide, + Pair: p, + Price: config.OrderSubmission.Price, + Amount: config.OrderSubmission.Amount, } var r12 string r12, err = e.ModifyOrder(&modifyRequest) @@ -506,9 +506,9 @@ func testWrappers(e exchange.IBotExchange, base *exchange.Base, config *Config) }) // r13 cancelRequest := order.Cancel{ - Side: testOrderSide, - CurrencyPair: p, - OrderID: config.OrderSubmission.OrderID, + Side: testOrderSide, + Pair: p, + ID: config.OrderSubmission.OrderID, } err = e.CancelOrder(&cancelRequest) msg = "" @@ -552,9 +552,9 @@ func testWrappers(e exchange.IBotExchange, base *exchange.Base, config *Config) }) historyRequest := order.GetOrdersRequest{ - OrderType: testOrderType, - OrderSide: testOrderSide, - Currencies: []currency.Pair{p}, + Type: testOrderType, + Side: testOrderSide, + Pairs: []currency.Pair{p}, } var r16 []order.Detail r16, err = e.GetOrderHistory(&historyRequest) @@ -571,9 +571,9 @@ func testWrappers(e exchange.IBotExchange, base *exchange.Base, config *Config) }) orderRequest := order.GetOrdersRequest{ - OrderType: testOrderType, - OrderSide: testOrderSide, - Currencies: []currency.Pair{p}, + Type: testOrderType, + Side: testOrderSide, + Pairs: []currency.Pair{p}, } var r17 []order.Detail r17, err = e.GetActiveOrders(&orderRequest) diff --git a/cmd/gctcli/commands.go b/cmd/gctcli/commands.go index 00b74534..34f5af6c 100644 --- a/cmd/gctcli/commands.go +++ b/cmd/gctcli/commands.go @@ -14,13 +14,12 @@ import ( "strings" "time" + "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/gctrpc" "github.com/urfave/cli" ) -const timeFormat = "2006-01-02 15:04:05" - var startTime, endTime, order string var limit int @@ -2462,13 +2461,13 @@ var withdrawalRequestCommand = cli.Command{ cli.StringFlag{ Name: "start", Usage: "", - Value: time.Now().AddDate(0, -1, 0).Format(timeFormat), + Value: time.Now().AddDate(0, -1, 0).Format(common.SimpleTimeFormat), Destination: &startTime, }, cli.StringFlag{ Name: "end", Usage: "", - Value: time.Now().Format(timeFormat), + Value: time.Now().Format(common.SimpleTimeFormat), Destination: &endTime, }, cli.Int64Flag{ @@ -2621,12 +2620,12 @@ func withdrawlRequestByDate(c *cli.Context) error { limit = limitStr } - s, err := time.Parse(timeFormat, startTime) + s, err := time.Parse(common.SimpleTimeFormat, startTime) if err != nil { return fmt.Errorf("invalid time format for start: %v", err) } - e, err := time.Parse(timeFormat, endTime) + e, err := time.Parse(common.SimpleTimeFormat, endTime) if err != nil { return fmt.Errorf("invalid time format for end: %v", err) } @@ -2648,8 +2647,8 @@ func withdrawlRequestByDate(c *cli.Context) error { result, err := client.WithdrawalEventsByDate(context.Background(), &gctrpc.WithdrawalEventsByDateRequest{ Exchange: exchange, - Start: s.In(loc).Format(timeFormat), - End: e.In(loc).Format(timeFormat), + Start: s.In(loc).Format(common.SimpleTimeFormat), + End: e.In(loc).Format(common.SimpleTimeFormat), Limit: int32(limit), }, ) @@ -3433,13 +3432,13 @@ var getAuditEventCommand = cli.Command{ cli.StringFlag{ Name: "start, s", Usage: "start date to search", - Value: time.Now().Add(-time.Hour).Format(timeFormat), + Value: time.Now().Add(-time.Hour).Format(common.SimpleTimeFormat), Destination: &startTime, }, cli.StringFlag{ Name: "end, e", Usage: "end time to search", - Value: time.Now().Format(timeFormat), + Value: time.Now().Format(common.SimpleTimeFormat), Destination: &endTime, }, cli.StringFlag{ @@ -3485,12 +3484,12 @@ func getAuditEvent(c *cli.Context) error { } } - s, err := time.Parse(timeFormat, startTime) + s, err := time.Parse(common.SimpleTimeFormat, startTime) if err != nil { return fmt.Errorf("invalid time format for start: %v", err) } - e, err := time.Parse(timeFormat, endTime) + e, err := time.Parse(common.SimpleTimeFormat, endTime) if err != nil { return fmt.Errorf("invalid time format for end: %v", err) } @@ -3513,8 +3512,8 @@ func getAuditEvent(c *cli.Context) error { result, err := client.GetAuditEvent(context.Background(), &gctrpc.GetAuditEventRequest{ - StartDate: s.In(loc).Format(timeFormat), - EndDate: e.In(loc).Format(timeFormat), + StartDate: s.In(loc).Format(common.SimpleTimeFormat), + EndDate: e.In(loc).Format(common.SimpleTimeFormat), Limit: int32(limit), OrderBy: order, Offset: int32(offset), diff --git a/common/common.go b/common/common.go index ef0e9bda..52a2205f 100644 --- a/common/common.go +++ b/common/common.go @@ -42,6 +42,9 @@ const ( WeiPerEther = 1000000000000000000 ) +// SimpleTimeFormat a common, but non-implemented time format in golang +const SimpleTimeFormat = "2006-01-02 15:04:05" + func initialiseHTTPClient() { // If the HTTPClient isn't set, start a new client with a default timeout of 15 seconds if HTTPClient == nil { diff --git a/database/repository/audit/audit.go b/database/repository/audit/audit.go index c759f7de..a34752c3 100644 --- a/database/repository/audit/audit.go +++ b/database/repository/audit/audit.go @@ -13,9 +13,6 @@ import ( "github.com/thrasher-corp/sqlboiler/queries/qm" ) -// TableTimeFormat Go Time format conversion -const TableTimeFormat = "2006-01-02 15:04:05" - // Event inserts a new audit event to database func Event(id, msgtype, message string) { if database.DB.SQL == nil { diff --git a/engine/events_test.go b/engine/events_test.go index f8bf2cdc..ea1d9ce8 100644 --- a/engine/events_test.go +++ b/engine/events_test.go @@ -13,10 +13,6 @@ const ( testExchange = "Bitstamp" ) -var ( - configLoaded = false -) - func addValidEvent() (int64, error) { return Add(testExchange, ItemPrice, @@ -27,10 +23,7 @@ func addValidEvent() (int64, error) { } func TestAdd(t *testing.T) { - if !configLoaded { - loadConfig(t) - } - + SetupTestHelpers(t) _, err := Add("", "", EventConditionParams{}, currency.Pair{}, "", "") if err == nil { t.Error("should err on invalid params") @@ -52,10 +45,7 @@ func TestAdd(t *testing.T) { } func TestRemove(t *testing.T) { - if !configLoaded { - loadConfig(t) - } - + SetupTestHelpers(t) id, err := addValidEvent() if err != nil { t.Error("unexpected result", err) @@ -71,10 +61,7 @@ func TestRemove(t *testing.T) { } func TestGetEventCounter(t *testing.T) { - if !configLoaded { - loadConfig(t) - } - + SetupTestHelpers(t) _, err := addValidEvent() if err != nil { t.Error("unexpected result", err) @@ -254,10 +241,7 @@ func TestCheckEventCondition(t *testing.T) { } func TestIsValidEvent(t *testing.T) { - if !configLoaded { - loadConfig(t) - } - + SetupTestHelpers(t) // invalid exchange name if err := IsValidEvent("meow", "", EventConditionParams{}, ""); err != errExchangeDisabled { t.Error("unexpected result:", err) @@ -308,9 +292,7 @@ func TestIsValidExchange(t *testing.T) { if s := IsValidExchange("invalidexchangerino"); s { t.Error("unexpected result") } - if !configLoaded { - loadConfig(t) - } + SetupTestHelpers(t) if s := IsValidExchange(testExchange); !s { t.Error("unexpected result") } diff --git a/engine/exchange_test.go b/engine/exchange_test.go index 73cf8388..ef586830 100644 --- a/engine/exchange_test.go +++ b/engine/exchange_test.go @@ -6,45 +6,29 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/bitfinex" ) -var testSetup = false - -func SetupTest(t *testing.T) { - if !testSetup { - var err error - Bot, err = New() - if err != nil { - t.Fatal(err) - } - testSetup = true - } - - if GetExchangeByName(testExchange) != nil { - return - } - err := LoadExchange(testExchange, false, nil) - if err != nil { - t.Errorf("SetupTest: Failed to load exchange: %s", err) - } -} - func CleanupTest(t *testing.T) { - if GetExchangeByName(testExchange) == nil { - return + if GetExchangeByName(testExchange) != nil { + err := UnloadExchange(testExchange) + if err != nil { + t.Fatalf("CleanupTest: Failed to unload exchange: %s", + err) + } } - - err := UnloadExchange(testExchange) - if err != nil { - t.Fatalf("CleanupTest: Failed to unload exchange: %s", - err) + if GetExchangeByName(fakePassExchange) != nil { + err := UnloadExchange(fakePassExchange) + if err != nil { + t.Fatalf("CleanupTest: Failed to unload exchange: %s", + err) + } } } func TestExchangeManagerAdd(t *testing.T) { t.Parallel() var e exchangeManager - bitfinex := new(bitfinex.Bitfinex) - bitfinex.SetDefaults() - e.add(bitfinex) + b := new(bitfinex.Bitfinex) + b.SetDefaults() + e.add(b) if exch := e.getExchanges(); exch[0].GetName() != "Bitfinex" { t.Error("unexpected exchange name") } @@ -56,9 +40,9 @@ func TestExchangeManagerGetExchanges(t *testing.T) { if exchanges := e.getExchanges(); exchanges != nil { t.Error("unexpected value") } - bitfinex := new(bitfinex.Bitfinex) - bitfinex.SetDefaults() - e.add(bitfinex) + b := new(bitfinex.Bitfinex) + b.SetDefaults() + e.add(b) if exch := e.getExchanges(); exch[0].GetName() != "Bitfinex" { t.Error("unexpected exchange name") } @@ -70,9 +54,9 @@ func TestExchangeManagerRemoveExchange(t *testing.T) { if err := e.removeExchange("Bitfinex"); err != ErrNoExchangesLoaded { t.Error("no exchanges should be loaded") } - bitfinex := new(bitfinex.Bitfinex) - bitfinex.SetDefaults() - e.add(bitfinex) + b := new(bitfinex.Bitfinex) + b.SetDefaults() + e.add(b) if err := e.removeExchange(testExchange); err != ErrExchangeNotFound { t.Error("Bitstamp exchange should return an error") } @@ -85,7 +69,7 @@ func TestExchangeManagerRemoveExchange(t *testing.T) { } func TestCheckExchangeExists(t *testing.T) { - SetupTest(t) + SetupTestHelpers(t) if GetExchangeByName(testExchange) == nil { t.Errorf("TestGetExchangeExists: Unable to find exchange") @@ -99,7 +83,7 @@ func TestCheckExchangeExists(t *testing.T) { } func TestGetExchangeByName(t *testing.T) { - SetupTest(t) + SetupTestHelpers(t) exch := GetExchangeByName(testExchange) if exch == nil { @@ -129,7 +113,7 @@ func TestGetExchangeByName(t *testing.T) { } func TestUnloadExchange(t *testing.T) { - SetupTest(t) + SetupTestHelpers(t) err := UnloadExchange("asdf") if err.Error() != "exchange asdf not found" { @@ -143,6 +127,12 @@ func TestUnloadExchange(t *testing.T) { err) } + err = UnloadExchange(fakePassExchange) + if err != nil { + t.Errorf("TestUnloadExchange: Failed to unload exchange. %s", + err) + } + err = UnloadExchange(testExchange) if err != ErrNoExchangesLoaded { t.Errorf("TestUnloadExchange: Incorrect result: %s", @@ -153,7 +143,7 @@ func TestUnloadExchange(t *testing.T) { } func TestDryRunParamInteraction(t *testing.T) { - SetupTest(t) + SetupTestHelpers(t) // Load bot as per normal, dry run and verbose for Bitfinex should be // disabled diff --git a/engine/fake_exchange_test.go b/engine/fake_exchange_test.go new file mode 100644 index 00000000..362b3d60 --- /dev/null +++ b/engine/fake_exchange_test.go @@ -0,0 +1,192 @@ +package engine + +import ( + "sync" + "time" + + "github.com/thrasher-corp/gocryptotrader/config" + "github.com/thrasher-corp/gocryptotrader/currency" + exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/account" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" + "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" + "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" + "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" + "github.com/thrasher-corp/gocryptotrader/portfolio/withdraw" +) + +const ( + fakePassExchange = "FakePassExchange" +) + +// FakePassingExchange is used to override IBotExchange responses in tests +// In this context, we don't care what FakePassingExchange does as we're testing +// the engine package +type FakePassingExchange struct { + exchange.Base +} + +// addPassingFakeExchange adds an exchange to engine tests where all funcs return a positive result +func addPassingFakeExchange(baseExchangeName string) error { + testExch := GetExchangeByName(baseExchangeName) + if testExch == nil { + return ErrExchangeNotFound + } + base := testExch.GetBase() + Bot.Config.Exchanges = append(Bot.Config.Exchanges, config.ExchangeConfig{ + Name: fakePassExchange, + Enabled: true, + Verbose: false, + }) + + Bot.exchangeManager.add(&FakePassingExchange{ + Base: exchange.Base{ + Name: fakePassExchange, + Enabled: true, + LoadedByConfig: true, + SkipAuthCheck: true, + API: base.API, + Features: base.Features, + HTTPTimeout: base.HTTPTimeout, + HTTPUserAgent: base.HTTPUserAgent, + HTTPRecording: base.HTTPRecording, + HTTPDebugging: base.HTTPDebugging, + WebsocketResponseCheckTimeout: base.WebsocketResponseCheckTimeout, + WebsocketResponseMaxLimit: base.WebsocketResponseMaxLimit, + WebsocketOrderbookBufferLimit: base.WebsocketOrderbookBufferLimit, + Websocket: base.Websocket, + Requester: base.Requester, + Config: base.Config, + }, + }) + return nil +} + +func (h *FakePassingExchange) Setup(_ *config.ExchangeConfig) error { return nil } +func (h *FakePassingExchange) Start(_ *sync.WaitGroup) {} +func (h *FakePassingExchange) SetDefaults() {} +func (h *FakePassingExchange) GetName() string { return fakePassExchange } +func (h *FakePassingExchange) IsEnabled() bool { return true } +func (h *FakePassingExchange) SetEnabled(bool) {} +func (h *FakePassingExchange) ValidateCredentials() error { return nil } + +func (h *FakePassingExchange) FetchTicker(_ currency.Pair, _ asset.Item) (*ticker.Price, error) { + return nil, nil +} +func (h *FakePassingExchange) UpdateTicker(_ currency.Pair, _ asset.Item) (*ticker.Price, error) { + return nil, nil +} +func (h *FakePassingExchange) FetchOrderbook(_ currency.Pair, _ asset.Item) (*orderbook.Base, error) { + return nil, nil +} +func (h *FakePassingExchange) UpdateOrderbook(_ currency.Pair, _ asset.Item) (*orderbook.Base, error) { + return nil, nil +} +func (h *FakePassingExchange) FetchTradablePairs(_ asset.Item) ([]string, error) { + return nil, nil +} +func (h *FakePassingExchange) UpdateTradablePairs(_ bool) error { return nil } + +func (h *FakePassingExchange) GetEnabledPairs(_ asset.Item) currency.Pairs { + return currency.Pairs{} +} +func (h *FakePassingExchange) GetAvailablePairs(_ asset.Item) currency.Pairs { + return currency.Pairs{} +} +func (h *FakePassingExchange) FetchAccountInfo() (account.Holdings, error) { + return account.Holdings{}, nil +} + +func (h *FakePassingExchange) UpdateAccountInfo() (account.Holdings, error) { + return account.Holdings{}, nil +} +func (h *FakePassingExchange) GetAuthenticatedAPISupport(_ uint8) bool { return true } +func (h *FakePassingExchange) SetPairs(_ currency.Pairs, _ asset.Item, _ bool) error { + return nil +} +func (h *FakePassingExchange) GetAssetTypes() asset.Items { return asset.Items{asset.Spot} } +func (h *FakePassingExchange) GetExchangeHistory(_ currency.Pair, _ asset.Item) ([]exchange.TradeHistory, error) { + return nil, nil +} +func (h *FakePassingExchange) SupportsAutoPairUpdates() bool { return true } +func (h *FakePassingExchange) SupportsRESTTickerBatchUpdates() bool { return true } +func (h *FakePassingExchange) GetFeeByType(_ *exchange.FeeBuilder) (float64, error) { + return 0, nil +} +func (h *FakePassingExchange) GetLastPairsUpdateTime() int64 { return 0 } +func (h *FakePassingExchange) GetWithdrawPermissions() uint32 { return 0 } +func (h *FakePassingExchange) FormatWithdrawPermissions() string { return "" } +func (h *FakePassingExchange) SupportsWithdrawPermissions(_ uint32) bool { return true } +func (h *FakePassingExchange) GetFundingHistory() ([]exchange.FundHistory, error) { return nil, nil } +func (h *FakePassingExchange) SubmitOrder(_ *order.Submit) (order.SubmitResponse, error) { + return order.SubmitResponse{ + IsOrderPlaced: true, + FullyMatched: true, + OrderID: "FakePassingExchangeOrder", + }, nil +} +func (h *FakePassingExchange) ModifyOrder(_ *order.Modify) (string, error) { return "", nil } +func (h *FakePassingExchange) CancelOrder(_ *order.Cancel) error { return nil } +func (h *FakePassingExchange) CancelAllOrders(_ *order.Cancel) (order.CancelAllResponse, error) { + return order.CancelAllResponse{}, nil +} +func (h *FakePassingExchange) GetOrderInfo(_ string) (order.Detail, error) { + return order.Detail{}, nil +} +func (h *FakePassingExchange) GetDepositAddress(_ currency.Code, _ string) (string, error) { + return "", nil +} +func (h *FakePassingExchange) GetOrderHistory(_ *order.GetOrdersRequest) ([]order.Detail, error) { + return nil, nil +} +func (h *FakePassingExchange) GetActiveOrders(_ *order.GetOrdersRequest) ([]order.Detail, error) { + return []order.Detail{ + { + Price: 1337, + Amount: 1337, + Exchange: fakePassExchange, + ID: "fakeOrder", + Type: order.Market, + Side: order.Buy, + Status: order.Active, + AssetType: asset.Spot, + Date: time.Now(), + Pair: currency.NewPairFromString("BTCUSD"), + }, + }, nil +} +func (h *FakePassingExchange) SetHTTPClientUserAgent(_ string) {} +func (h *FakePassingExchange) GetHTTPClientUserAgent() string { return "" } +func (h *FakePassingExchange) SetClientProxyAddress(_ string) error { return nil } +func (h *FakePassingExchange) SupportsWebsocket() bool { return true } +func (h *FakePassingExchange) SupportsREST() bool { return true } +func (h *FakePassingExchange) IsWebsocketEnabled() bool { return true } +func (h *FakePassingExchange) GetWebsocket() (*wshandler.Websocket, error) { return nil, nil } +func (h *FakePassingExchange) SubscribeToWebsocketChannels(_ []wshandler.WebsocketChannelSubscription) error { + return nil +} +func (h *FakePassingExchange) UnsubscribeToWebsocketChannels(_ []wshandler.WebsocketChannelSubscription) error { + return nil +} +func (h *FakePassingExchange) AuthenticateWebsocket() error { return nil } +func (h *FakePassingExchange) GetSubscriptions() ([]wshandler.WebsocketChannelSubscription, error) { + return nil, nil +} +func (h *FakePassingExchange) GetDefaultConfig() (*config.ExchangeConfig, error) { return nil, nil } +func (h *FakePassingExchange) GetBase() *exchange.Base { return nil } +func (h *FakePassingExchange) SupportsAsset(_ asset.Item) bool { return true } +func (h *FakePassingExchange) GetHistoricCandles(_ currency.Pair, _, _ int64) ([]exchange.Candle, error) { + return []exchange.Candle{}, nil +} +func (h *FakePassingExchange) DisableRateLimiter() error { return nil } +func (h *FakePassingExchange) EnableRateLimiter() error { return nil } +func (h *FakePassingExchange) WithdrawCryptocurrencyFunds(_ *withdraw.Request) (*withdraw.ExchangeResponse, error) { + return nil, nil +} +func (h *FakePassingExchange) WithdrawFiatFunds(_ *withdraw.Request) (*withdraw.ExchangeResponse, error) { + return nil, nil +} +func (h *FakePassingExchange) WithdrawFiatFundsToInternationalBank(_ *withdraw.Request) (*withdraw.ExchangeResponse, error) { + return nil, nil +} diff --git a/engine/helpers_test.go b/engine/helpers_test.go index c53b0fe0..b4a83283 100644 --- a/engine/helpers_test.go +++ b/engine/helpers_test.go @@ -31,23 +31,33 @@ var ( func SetupTestHelpers(t *testing.T) { if !helperTestLoaded { - if !testSetup { - if Bot == nil { - Bot = new(Engine) - } - Bot.Config = &config.Cfg - err := Bot.Config.LoadConfig(config.TestFile, true) - if err != nil { - t.Fatalf("SetupTest: Failed to load config: %s", err) - } - testSetup = true + if Bot == nil { + Bot = new(Engine) } - err := Bot.Config.RetrieveConfigCurrencyPairs(true, asset.Spot) + Bot.Config = &config.Cfg + err := Bot.Config.LoadConfig(config.TestFile, true) + if err != nil { + t.Fatalf("SetupTest: Failed to load config: %s", err) + } + err = Bot.Config.RetrieveConfigCurrencyPairs(true, asset.Spot) if err != nil { t.Fatalf("Failed to retrieve config currency pairs. %s", err) } helperTestLoaded = true } + + if GetExchangeByName(testExchange) == nil { + err := LoadExchange(testExchange, false, nil) + if err != nil { + t.Fatalf("SetupTest: Failed to load exchange: %s", err) + } + } + if GetExchangeByName(fakePassExchange) == nil { + err := addPassingFakeExchange(testExchange) + if err != nil { + t.Fatalf("SetupTest: Failed to load exchange: %s", err) + } + } } func TestGetExchangeOTPs(t *testing.T) { @@ -117,8 +127,8 @@ func TestGetExchangeoOTPByName(t *testing.T) { func TestGetAuthAPISupportedExchanges(t *testing.T) { SetupTestHelpers(t) - if result := GetAuthAPISupportedExchanges(); result != nil { - t.Fatal("Unexpected result") + if result := GetAuthAPISupportedExchanges(); len(result) != 1 { + t.Fatal("Unexpected result", result) } } @@ -571,7 +581,7 @@ func TestGetCryptocurrenciesByExchange(t *testing.T) { } func TestGetExchangeNames(t *testing.T) { - SetupTest(t) + SetupTestHelpers(t) if e := GetExchangeNames(true); len(e) == 0 { t.Error("exchange names should be populated") } @@ -581,8 +591,8 @@ func TestGetExchangeNames(t *testing.T) { if e := GetExchangeNames(true); common.StringDataCompare(e, testExchange) { t.Error("Bitstamp should be missing") } - if e := GetExchangeNames(false); len(e) != 27 { - t.Error("len should be all available exchanges") + if e := GetExchangeNames(false); len(e) != len(Bot.Config.Exchanges) { + t.Errorf("Expected %v Received %v", len(e), len(Bot.Config.Exchanges)) } } diff --git a/engine/orders.go b/engine/orders.go index fbfc2a76..50cc0687 100644 --- a/engine/orders.go +++ b/engine/orders.go @@ -17,15 +17,66 @@ import ( var ( OrderManagerDelay = time.Second * 10 ErrOrdersAlreadyExists = errors.New("order already exists") + ErrOrderNotFound = errors.New("order does not exist") ) -func (o *orderStore) Get() map[string][]order.Detail { - o.m.Lock() - defer o.m.Unlock() +// get returns all orders for all exchanges +// should not be exported as it can have large impact if used improperly +func (o *orderStore) get() map[string][]*order.Detail { + o.m.RLock() + defer o.m.RUnlock() return o.Orders } +// GetByExchangeAndID returns a specific order by exchange and id +func (o *orderStore) GetByExchangeAndID(exchange, id string) (*order.Detail, error) { + o.m.RLock() + defer o.m.RUnlock() + r, ok := o.Orders[exchange] + if !ok { + return nil, ErrExchangeNotFound + } + + for x := range r { + if r[x].ID == id { + return r[x], nil + } + } + return nil, ErrOrderNotFound +} + +// GetByExchange returns orders by exchange +func (o *orderStore) GetByExchange(exchange string) ([]*order.Detail, error) { + o.m.RLock() + defer o.m.RUnlock() + r, ok := o.Orders[exchange] + if !ok { + return nil, ErrExchangeNotFound + } + return r, nil +} + +// GetByInternalOrderID will search all orders for our internal orderID +// and return the order +func (o *orderStore) GetByInternalOrderID(internalOrderID string) (*order.Detail, error) { + o.m.RLock() + defer o.m.RUnlock() + for _, v := range o.Orders { + for x := range v { + if v[x].InternalOrderID == internalOrderID { + return v[x], nil + } + } + } + return nil, ErrOrderNotFound +} + func (o *orderStore) exists(order *order.Detail) bool { + if order == nil { + return false + } + o.m.RLock() + defer o.m.RUnlock() r, ok := o.Orders[order.Exchange] if !ok { return false @@ -36,28 +87,47 @@ func (o *orderStore) exists(order *order.Detail) bool { return true } } - return false } +// Add Adds an order to the orderStore for tracking the lifecycle func (o *orderStore) Add(order *order.Detail) error { - o.m.Lock() - defer o.m.Unlock() - + if order == nil { + return errors.New("order store: Order is nil") + } + exch := GetExchangeByName(order.Exchange) + if exch == nil { + return ErrExchangeNotFound + } if o.exists(order) { return ErrOrdersAlreadyExists } - + // Untracked websocket orders will not have internalIDs yet + if order.InternalOrderID == "" { + id, err := uuid.NewV4() + if err != nil { + log.Warnf(log.OrderMgr, + "Order manager: Unable to generate UUID. Err: %s", + err) + } else { + order.InternalOrderID = id.String() + } + } + o.m.Lock() + defer o.m.Unlock() orders := o.Orders[order.Exchange] - orders = append(orders, *order) + orders = append(orders, order) o.Orders[order.Exchange] = orders + return nil } +// Started returns the status of the orderManager func (o *orderManager) Started() bool { return atomic.LoadInt32(&o.started) == 1 } +// Start will boot up the orderManager func (o *orderManager) Start() error { if atomic.AddInt32(&o.started, 1) != 1 { return errors.New("order manager already started") @@ -66,11 +136,12 @@ func (o *orderManager) Start() error { log.Debugln(log.OrderBook, "Order manager starting...") o.shutdown = make(chan struct{}) - o.orderStore.Orders = make(map[string][]order.Detail) + o.orderStore.Orders = make(map[string][]*order.Detail) go o.run() return nil } +// Stop will attempt to shutdown the orderManager func (o *orderManager) Stop() error { if atomic.LoadInt32(&o.started) == 0 { return errors.New("order manager not started") @@ -92,39 +163,7 @@ func (o *orderManager) Stop() error { func (o *orderManager) gracefulShutdown() { if o.cfg.CancelOrdersOnShutdown { log.Debugln(log.OrderMgr, "Order manager: Cancelling any open orders...") - orders := o.orderStore.Get() - if orders == nil { - return - } - - for k, v := range orders { - log.Debugf(log.OrderMgr, "Order manager: Cancelling order(s) for exchange %s.\n", k) - for y := range v { - log.Debugf(log.OrderMgr, "order manager: Cancelling order ID %v [%v]", - v[y].ID, v[y]) - err := o.Cancel(k, &order.Cancel{ - OrderID: v[y].ID, - }) - if err != nil { - msg := fmt.Sprintf("Order manager: Exchange %s unable to cancel order ID=%v. Err: %s", - k, v[y].ID, err) - log.Debugln(log.OrderBook, msg) - Bot.CommsManager.PushEvent(base.Event{ - Type: "order", - Message: msg, - }) - continue - } - - msg := fmt.Sprintf("Order manager: Exchange %s order ID=%v cancelled.", - k, v[y].ID) - log.Debugln(log.OrderBook, msg) - Bot.CommsManager.PushEvent(base.Event{ - Type: "order", - Message: msg, - }) - } - } + o.CancelAllOrders(Bot.Config.GetEnabledExchanges()) } } @@ -149,35 +188,98 @@ func (o *orderManager) run() { } } -func (o *orderManager) CancelAllOrders() {} - -func (o *orderManager) Cancel(exchName string, cancel *order.Cancel) error { - if exchName == "" { - return errors.New("order exchange name is empty") +// CancelAllOrders iterates and cancels all orders for each exchange provided +func (o *orderManager) CancelAllOrders(exchangeNames []string) { + orders := o.orderStore.get() + if orders == nil { + return } + for k, v := range orders { + log.Debugf(log.OrderMgr, "Order manager: Cancelling order(s) for exchange %s.", k) + if !common.StringDataCompareInsensitive(exchangeNames, k) { + continue + } + + for y := range v { + log.Debugf(log.OrderMgr, "Order manager: Cancelling order ID %v [%v]", + v[y].ID, v[y]) + err := o.Cancel(&order.Cancel{ + Exchange: k, + ID: v[y].ID, + AccountID: v[y].AccountID, + ClientID: v[y].ClientID, + WalletAddress: v[y].WalletAddress, + Type: v[y].Type, + Side: v[y].Side, + Pair: v[y].Pair, + }) + if err != nil { + log.Error(log.OrderMgr, err) + Bot.CommsManager.PushEvent(base.Event{ + Type: "order", + Message: err.Error(), + }) + continue + } + + msg := fmt.Sprintf("Order manager: Exchange %s order ID=%v cancelled.", + k, v[y].ID) + log.Debugln(log.OrderMgr, msg) + Bot.CommsManager.PushEvent(base.Event{ + Type: "order", + Message: msg, + }) + } + } +} + +// Cancel will find the order in the orderManager, send a cancel request +// to the exchange and if successful, update the status of the order +func (o *orderManager) Cancel(cancel *order.Cancel) error { if cancel == nil { return errors.New("order cancel param is nil") } - if cancel.OrderID == "" { + if cancel.Exchange == "" { + return errors.New("order exchange name is empty") + } + + if cancel.ID == "" { return errors.New("order id is empty") } - exch := GetExchangeByName(exchName) + exch := GetExchangeByName(cancel.Exchange) if exch == nil { - return errors.New("unable to get exchange by name") + return ErrExchangeNotFound } if cancel.AssetType.String() != "" && !exch.GetAssetTypes().Contains(cancel.AssetType) { return errors.New("order asset type not supported by exchange") } - return exch.CancelOrder(cancel) + err := exch.CancelOrder(cancel) + if err != nil { + return fmt.Errorf("%v - Failed to cancel order: %v", cancel.Exchange, err) + } + var od *order.Detail + od, err = o.orderStore.GetByExchangeAndID(cancel.Exchange, cancel.ID) + if err != nil { + return fmt.Errorf("%v - Failed to retrieve order %v to update cancelled status: %v", cancel.Exchange, cancel.ID, err) + } + + od.Status = order.Cancelled + return nil } -func (o *orderManager) Submit(exchName string, newOrder *order.Submit) (*orderSubmitResponse, error) { - if exchName == "" { +// Submit will take in an order struct, send it to the exchange and +// populate it in the orderManager if successful +func (o *orderManager) Submit(newOrder *order.Submit) (*orderSubmitResponse, error) { + if newOrder == nil { + return nil, errors.New("order cannot be nil") + } + + if newOrder.Exchange == "" { return nil, errors.New("order exchange name must be specified") } @@ -186,7 +288,7 @@ func (o *orderManager) Submit(exchName string, newOrder *order.Submit) (*orderSu } if o.cfg.EnforceLimitConfig { - if !o.cfg.AllowMarketOrders && newOrder.OrderType == order.Market { + if !o.cfg.AllowMarketOrders && newOrder.Type == order.Market { return nil, errors.New("order market type is not allowed") } @@ -195,7 +297,7 @@ func (o *orderManager) Submit(exchName string, newOrder *order.Submit) (*orderSu } if len(o.cfg.AllowedExchanges) > 0 && - !common.StringDataCompareInsensitive(o.cfg.AllowedExchanges, exchName) { + !common.StringDataCompareInsensitive(o.cfg.AllowedExchanges, newOrder.Exchange) { return nil, errors.New("order exchange not found in allowed list") } @@ -204,18 +306,10 @@ func (o *orderManager) Submit(exchName string, newOrder *order.Submit) (*orderSu } } - exch := GetExchangeByName(exchName) + exch := GetExchangeByName(newOrder.Exchange) if exch == nil { - return nil, errors.New("unable to get exchange by name") + return nil, ErrExchangeNotFound } - - id, err := uuid.NewV4() - if err != nil { - log.Warnf(log.OrderMgr, - "Order manager: Unable to generate UUID. Err: %s\n", - err) - } - result, err := exch.SubmitOrder(newOrder) if err != nil { return nil, err @@ -225,42 +319,84 @@ func (o *orderManager) Submit(exchName string, newOrder *order.Submit) (*orderSu return nil, errors.New("order unable to be placed") } + var id uuid.UUID + id, err = uuid.NewV4() + if err != nil { + log.Warnf(log.OrderMgr, + "Order manager: Unable to generate UUID. Err: %s", + err) + } msg := fmt.Sprintf("Order manager: Exchange %s submitted order ID=%v [Ours: %v] pair=%v price=%v amount=%v side=%v type=%v.", - exchName, + newOrder.Exchange, result.OrderID, id.String(), newOrder.Pair, newOrder.Price, newOrder.Amount, - newOrder.OrderSide, - newOrder.OrderType) + newOrder.Side, + newOrder.Type) log.Debugln(log.OrderMgr, msg) Bot.CommsManager.PushEvent(base.Event{ Type: "order", Message: msg, }) + status := order.New + if result.FullyMatched { + status = order.Filled + } + err = o.orderStore.Add(&order.Detail{ + ImmediateOrCancel: newOrder.ImmediateOrCancel, + HiddenOrder: newOrder.HiddenOrder, + FillOrKill: newOrder.FillOrKill, + PostOnly: newOrder.PostOnly, + Price: newOrder.Price, + Amount: newOrder.Amount, + LimitPriceUpper: newOrder.LimitPriceUpper, + LimitPriceLower: newOrder.LimitPriceLower, + TriggerPrice: newOrder.TriggerPrice, + TargetAmount: newOrder.TargetAmount, + ExecutedAmount: newOrder.ExecutedAmount, + RemainingAmount: newOrder.RemainingAmount, + Fee: newOrder.Fee, + Exchange: newOrder.Exchange, + InternalOrderID: id.String(), + ID: result.OrderID, + AccountID: newOrder.AccountID, + ClientID: newOrder.ClientID, + WalletAddress: newOrder.WalletAddress, + Type: newOrder.Type, + Side: newOrder.Side, + Status: status, + AssetType: newOrder.AssetType, + Date: time.Now(), + LastUpdated: time.Now(), + Pair: newOrder.Pair, + }) + if err != nil { + return nil, fmt.Errorf("unable to add %v order %v to orderStore: %s", newOrder.Exchange, result.OrderID, err) + } return &orderSubmitResponse{ SubmitResponse: order.SubmitResponse{ OrderID: result.OrderID, }, - OurOrderID: id.String(), + InternalOrderID: id.String(), }, nil } func (o *orderManager) processOrders() { authExchanges := GetAuthAPISupportedExchanges() for x := range authExchanges { - log.Debugf(log.OrderMgr, "Order manager: Procesing orders for exchange %v.\n", authExchanges[x]) + log.Debugf(log.OrderMgr, "Order manager: Procesing orders for exchange %v.", authExchanges[x]) exch := GetExchangeByName(authExchanges[x]) req := order.GetOrdersRequest{ - OrderSide: order.AnySide, - OrderType: order.AnyType, + Side: order.AnySide, + Type: order.AnyType, } result, err := exch.GetActiveOrders(&req) if err != nil { - log.Warnf(log.OrderMgr, "Order manager: Unable to get active orders: %s\n", err) + log.Warnf(log.OrderMgr, "Order manager: Unable to get active orders: %s", err) continue } @@ -269,8 +405,8 @@ func (o *orderManager) processOrders() { result := o.orderStore.Add(ord) if result != ErrOrdersAlreadyExists { msg := fmt.Sprintf("Order manager: Exchange %s added order ID=%v pair=%v price=%v amount=%v side=%v type=%v.", - ord.Exchange, ord.ID, ord.CurrencyPair, ord.Price, ord.Amount, ord.OrderSide, ord.OrderType) - log.Debugf(log.OrderMgr, "%v\n", msg) + ord.Exchange, ord.ID, ord.Pair, ord.Price, ord.Amount, ord.Side, ord.Type) + log.Debugf(log.OrderMgr, "%v", msg) Bot.CommsManager.PushEvent(base.Event{ Type: "order", Message: msg, diff --git a/engine/orders_test.go b/engine/orders_test.go new file mode 100644 index 00000000..1e8a0838 --- /dev/null +++ b/engine/orders_test.go @@ -0,0 +1,379 @@ +package engine + +import ( + "testing" + "time" + + "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" +) + +var ordersSetupRan bool + +func OrdersSetup(t *testing.T) { + SetupTestHelpers(t) + if !ordersSetupRan { + err := Bot.OrderManager.Start() + if err != nil { + t.Fatal(err) + } + if !Bot.OrderManager.Started() { + t.Fatal("Order manager not started") + } + ordersSetupRan = true + } +} + +func TestOrdersGet(t *testing.T) { + OrdersSetup(t) + if Bot.OrderManager.orderStore.get() == nil { + t.Error("orderStore not established") + } +} + +func TestOrdersAdd(t *testing.T) { + OrdersSetup(t) + err := Bot.OrderManager.orderStore.Add(&order.Detail{ + Exchange: testExchange, + ID: "TestOrdersAdd", + }) + if err != nil { + t.Error(err) + } + err = Bot.OrderManager.orderStore.Add(&order.Detail{ + Exchange: "testTest", + ID: "TestOrdersAdd", + }) + if err == nil { + t.Error("Expected error from non existent exchange") + } + + err = Bot.OrderManager.orderStore.Add(nil) + if err == nil { + t.Error("Expected error from nil order") + } + + err = Bot.OrderManager.orderStore.Add(&order.Detail{ + Exchange: testExchange, + ID: "TestOrdersAdd", + }) + if err == nil { + t.Error("Expected error re-adding order") + } +} + +func TestGetByInternalOrderID(t *testing.T) { + OrdersSetup(t) + err := Bot.OrderManager.orderStore.Add(&order.Detail{ + Exchange: testExchange, + ID: "TestGetByInternalOrderID", + InternalOrderID: "internalTest", + }) + if err != nil { + t.Error(err) + } + + o, err := Bot.OrderManager.orderStore.GetByInternalOrderID("internalTest") + if err != nil { + t.Error(err) + } + if o == nil { + t.Fatal("Expected a matching order") + } + if o.ID != "TestGetByInternalOrderID" { + t.Error("Expected to retrieve order") + } + + _, err = Bot.OrderManager.orderStore.GetByInternalOrderID("NoOrder") + if err != ErrOrderNotFound { + t.Error(err) + } +} + +func TestGetByExchange(t *testing.T) { + OrdersSetup(t) + err := Bot.OrderManager.orderStore.Add(&order.Detail{ + Exchange: testExchange, + ID: "TestGetByExchange", + InternalOrderID: "internalTestGetByExchange", + }) + if err != nil { + t.Error(err) + } + + err = Bot.OrderManager.orderStore.Add(&order.Detail{ + Exchange: testExchange, + ID: "TestGetByExchange2", + InternalOrderID: "internalTestGetByExchange2", + }) + if err != nil { + t.Error(err) + } + + err = Bot.OrderManager.orderStore.Add(&order.Detail{ + Exchange: fakePassExchange, + ID: "TestGetByExchange3", + InternalOrderID: "internalTest3", + }) + if err != nil { + t.Error(err) + } + var o []*order.Detail + o, err = Bot.OrderManager.orderStore.GetByExchange(testExchange) + if err != nil { + t.Error(err) + } + if o == nil { + t.Error("Expected non nil response") + } + var o1Found, o2Found bool + for i := range o { + if o[i].ID == "TestGetByExchange" && o[i].Exchange == testExchange { + o1Found = true + } + if o[i].ID == "TestGetByExchange2" && o[i].Exchange == testExchange { + o2Found = true + } + } + if !o1Found || !o2Found { + t.Error("Expected orders 'TestGetByExchange' and 'TestGetByExchange2' to be returned") + } + + _, err = Bot.OrderManager.orderStore.GetByInternalOrderID("NoOrder") + if err != ErrOrderNotFound { + t.Error(err) + } + err = Bot.OrderManager.orderStore.Add(&order.Detail{ + Exchange: "thisWillFail", + }) + if err == nil { + t.Error("Expected exchange not found error") + } +} + +func TestGetByExchangeAndID(t *testing.T) { + OrdersSetup(t) + err := Bot.OrderManager.orderStore.Add(&order.Detail{ + Exchange: testExchange, + ID: "TestGetByExchangeAndID", + }) + if err != nil { + t.Error(err) + } + + o, err := Bot.OrderManager.orderStore.GetByExchangeAndID(testExchange, "TestGetByExchangeAndID") + if err != nil { + t.Error(err) + } + if o.ID != "TestGetByExchangeAndID" { + t.Error("Expected to retrieve order") + } + + _, err = Bot.OrderManager.orderStore.GetByExchangeAndID("", "TestGetByExchangeAndID") + if err != ErrExchangeNotFound { + t.Error(err) + } + + _, err = Bot.OrderManager.orderStore.GetByExchangeAndID(testExchange, "") + if err != ErrOrderNotFound { + t.Error(err) + } +} + +func TestExists(t *testing.T) { + OrdersSetup(t) + if Bot.OrderManager.orderStore.exists(nil) { + t.Error("Expected false") + } + o := &order.Detail{ + Exchange: testExchange, + ID: "TestExists", + } + err := Bot.OrderManager.orderStore.Add(o) + if err != nil { + t.Error(err) + } + b := Bot.OrderManager.orderStore.exists(o) + if !b { + t.Error("Expected true") + } +} + +func TestCancelOrder(t *testing.T) { + OrdersSetup(t) + err := Bot.OrderManager.Cancel(nil) + if err == nil { + t.Error("Expected error due to empty order") + } + + err = Bot.OrderManager.Cancel(&order.Cancel{}) + if err == nil { + t.Error("Expected error due to empty order") + } + + err = Bot.OrderManager.Cancel(&order.Cancel{ + Exchange: testExchange, + }) + if err == nil { + t.Error("Expected error due to no order ID") + } + + err = Bot.OrderManager.Cancel(&order.Cancel{ + ID: "ID", + }) + if err == nil { + t.Error("Expected error due to no Exchange") + } + + err = Bot.OrderManager.Cancel(&order.Cancel{ + ID: "ID", + Exchange: testExchange, + AssetType: asset.Binary, + }) + if err == nil { + t.Error("Expected error due to bad asset type") + } + + o := &order.Detail{ + Exchange: fakePassExchange, + ID: "TestCancelOrder", + Status: order.New, + } + err = Bot.OrderManager.orderStore.Add(o) + if err != nil { + t.Error(err) + } + + err = Bot.OrderManager.Cancel(&order.Cancel{ + ID: "Unknown", + Exchange: fakePassExchange, + AssetType: asset.Spot, + }) + if err == nil { + t.Error("Expected error due to no order found") + } + + cancel := &order.Cancel{ + Exchange: fakePassExchange, + ID: "TestCancelOrder", + Side: order.Sell, + Status: order.New, + AssetType: asset.Spot, + Date: time.Now(), + Pair: currency.NewPairFromString("BTCUSD"), + } + err = Bot.OrderManager.Cancel(cancel) + if err != nil { + t.Error(err) + } + + if o.Status != order.Cancelled { + t.Error("Failed to cancel") + } +} + +func TestCancelAllOrders(t *testing.T) { + OrdersSetup(t) + o := &order.Detail{ + Exchange: fakePassExchange, + ID: "TestCancelAllOrders", + Status: order.New, + } + err := Bot.OrderManager.orderStore.Add(o) + if err != nil { + t.Error(err) + } + + Bot.OrderManager.CancelAllOrders([]string{"NotFound"}) + if o.Status == order.Cancelled { + t.Error("Order should not be cancelled") + } + + Bot.OrderManager.CancelAllOrders([]string{fakePassExchange}) + if o.Status != order.Cancelled { + t.Error("Order should be cancelled") + } + + o.Status = order.New + Bot.OrderManager.CancelAllOrders(nil) + if o.Status != order.New { + t.Error("Order should not be cancelled") + } +} + +func TestSubmit(t *testing.T) { + OrdersSetup(t) + _, err := Bot.OrderManager.Submit(nil) + if err == nil { + t.Error("Expected error from nil order") + } + + o := &order.Submit{ + Exchange: "", + ID: "FakePassingExchangeOrder", + Status: order.New, + Type: order.Market, + } + _, err = Bot.OrderManager.Submit(o) + if err == nil { + t.Error("Expected error from empty exchange") + } + + o.Exchange = fakePassExchange + _, err = Bot.OrderManager.Submit(o) + if err == nil { + t.Error("Expected error from validation") + } + + Bot.OrderManager.cfg.EnforceLimitConfig = true + Bot.OrderManager.cfg.AllowMarketOrders = false + o.Pair = currency.NewPairFromString("BTCUSD") + o.AssetType = asset.Spot + o.Side = order.Buy + o.Amount = 1 + o.Price = 1 + _, err = Bot.OrderManager.Submit(o) + if err == nil { + t.Error("Expected fail due to order market type is not allowed") + } + Bot.OrderManager.cfg.AllowMarketOrders = true + Bot.OrderManager.cfg.LimitAmount = 1 + o.Amount = 2 + _, err = Bot.OrderManager.Submit(o) + if err == nil { + t.Error("Expected fail due to order limit exceeds allowed limit") + } + Bot.OrderManager.cfg.LimitAmount = 0 + Bot.OrderManager.cfg.AllowedExchanges = []string{"fake"} + _, err = Bot.OrderManager.Submit(o) + if err == nil { + t.Error("Expected fail due to order exchange not found in allowed list") + } + + Bot.OrderManager.cfg.AllowedExchanges = nil + Bot.OrderManager.cfg.AllowedPairs = currency.Pairs{currency.NewPairFromString("BTCAUD")} + _, err = Bot.OrderManager.Submit(o) + if err == nil { + t.Error("Expected fail due to order pair not found in allowed list") + } + + Bot.OrderManager.cfg.AllowedPairs = nil + _, err = Bot.OrderManager.Submit(o) + if err != nil { + t.Error(err) + } + + o2, err := Bot.OrderManager.orderStore.GetByExchangeAndID(fakePassExchange, "FakePassingExchangeOrder") + if err != nil { + t.Error(err) + } + if o2.InternalOrderID == "" { + t.Error("Failed to assign internal order id") + } +} + +func TestProcessOrders(t *testing.T) { + OrdersSetup(t) + Bot.OrderManager.processOrders() +} diff --git a/engine/orders_types.go b/engine/orders_types.go index c0479299..69ddfd00 100644 --- a/engine/orders_types.go +++ b/engine/orders_types.go @@ -18,8 +18,8 @@ type orderManagerConfig struct { } type orderStore struct { - m sync.Mutex - Orders map[string][]order.Detail + m sync.RWMutex + Orders map[string][]*order.Detail } type orderManager struct { @@ -32,5 +32,5 @@ type orderManager struct { type orderSubmitResponse struct { order.SubmitResponse - OurOrderID string + InternalOrderID string } diff --git a/engine/restful_server_test.go b/engine/restful_server_test.go index cb6af0cd..294b831c 100644 --- a/engine/restful_server_test.go +++ b/engine/restful_server_test.go @@ -12,16 +12,6 @@ import ( "github.com/thrasher-corp/gocryptotrader/config" ) -func loadConfig(t *testing.T) *config.Config { - cfg := config.GetConfig() - err := cfg.LoadConfig("", true) - if err != nil { - t.Error("GetCurrencyConfig LoadConfig error", err) - } - configLoaded = true - return cfg -} - func makeHTTPGetRequest(t *testing.T, response interface{}) *http.Response { w := httptest.NewRecorder() @@ -34,8 +24,8 @@ func makeHTTPGetRequest(t *testing.T, response interface{}) *http.Response { // TestConfigAllJsonResponse test if config/all restful json response is valid func TestConfigAllJsonResponse(t *testing.T) { - cfg := loadConfig(t) - resp := makeHTTPGetRequest(t, cfg) + SetupTestHelpers(t) + resp := makeHTTPGetRequest(t, Bot.Config) body, err := ioutil.ReadAll(resp.Body) resp.Body.Close() if err != nil { @@ -48,16 +38,13 @@ func TestConfigAllJsonResponse(t *testing.T) { t.Error("Response not parseable as json", err) } - if reflect.DeepEqual(responseConfig, cfg) { + if reflect.DeepEqual(responseConfig, Bot.Config) { t.Error("Json not equal to config") } } func TestInvalidHostRequest(t *testing.T) { - Bot = &Engine{ - Config: loadConfig(t), - } - + SetupTestHelpers(t) req, err := http.NewRequest(http.MethodGet, "/config/all", nil) if err != nil { t.Fatal(err) @@ -73,10 +60,7 @@ func TestInvalidHostRequest(t *testing.T) { } func TestValidHostRequest(t *testing.T) { - Bot = &Engine{ - Config: loadConfig(t), - } - + SetupTestHelpers(t) req, err := http.NewRequest(http.MethodGet, "/config/all", nil) if err != nil { t.Fatal(err) @@ -92,10 +76,7 @@ func TestValidHostRequest(t *testing.T) { } func TestProfilerEnabledShouldEnableProfileEndPoint(t *testing.T) { - Bot = &Engine{ - Config: loadConfig(t), - } - + SetupTestHelpers(t) req, err := http.NewRequest(http.MethodGet, "/debug/pprof/", nil) if err != nil { t.Fatal(err) diff --git a/engine/routines.go b/engine/routines.go index ee65b273..bc9e60a4 100644 --- a/engine/routines.go +++ b/engine/routines.go @@ -5,11 +5,11 @@ import ( "fmt" "strings" "sync" - "time" "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" "github.com/thrasher-corp/gocryptotrader/exchanges/stats" "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" @@ -232,7 +232,7 @@ func WebsocketRoutine() { } // Data handler routine - go WebsocketDataHandler(ws) + go WebsocketDataReceiver(ws) err = ws.Connect() if err != nil { @@ -252,35 +252,9 @@ func WebsocketRoutine() { var shutdowner = make(chan struct{}, 1) var wg sync.WaitGroup -// Websocketshutdown shuts down the exchange routines and then shuts down -// governing routines -func Websocketshutdown(ws *wshandler.Websocket) error { - err := ws.Shutdown() // shutdown routines on the exchange - if err != nil { - log.Errorf(log.WebsocketMgr, "routines.go error - failed to shutdown %s\n", err) - } - - timer := time.NewTimer(5 * time.Second) - c := make(chan struct{}, 1) - - go func(c chan struct{}) { - close(shutdowner) - wg.Wait() - c <- struct{}{} - }(c) - - select { - case <-timer.C: - return errors.New("routines.go error - failed to shutdown routines") - - case <-c: - return nil - } -} - -// WebsocketDataHandler handles websocket data coming from a websocket feed +// WebsocketDataReceiver handles websocket data coming from a websocket feed // associated with an exchange -func WebsocketDataHandler(ws *wshandler.Websocket) { +func WebsocketDataReceiver(ws *wshandler.Websocket) { wg.Add(1) defer wg.Done() @@ -288,85 +262,109 @@ func WebsocketDataHandler(ws *wshandler.Websocket) { select { case <-shutdowner: return - case data := <-ws.DataHandler: - switch d := data.(type) { - case string: - switch d { - case wshandler.WebsocketNotEnabled: - if Bot.Settings.Verbose { - log.Warnf(log.WebsocketMgr, "routines.go warning - exchange %s websocket not enabled\n", - ws.GetName()) - } - default: - log.Info(log.WebsocketMgr, d) - } - case error: - log.Errorf(log.WebsocketMgr, "routines.go exchange %s websocket error - %s", ws.GetName(), data) - case wshandler.TradeData: - // Websocket Trade Data - if Bot.Settings.Verbose { - log.Infof(log.WebsocketMgr, "%s websocket %s %s trade updated %+v\n", - ws.GetName(), - FormatCurrency(d.CurrencyPair), - d.AssetType, - d) - } - case wshandler.FundingData: - // Websocket Funding Data - if Bot.Settings.Verbose { - log.Infof(log.WebsocketMgr, "%s websocket %s %s funding updated %+v\n", - ws.GetName(), - FormatCurrency(d.CurrencyPair), - d.AssetType, - d) - } - case *ticker.Price: - // Websocket Ticker Data - if Bot.Settings.EnableExchangeSyncManager && Bot.ExchangeCurrencyPairManager != nil { - Bot.ExchangeCurrencyPairManager.update(ws.GetName(), - d.Pair, - d.AssetType, - SyncItemTicker, - nil) - } - err := ticker.ProcessTicker(ws.GetName(), d, d.AssetType) - printTickerSummary(d, d.Pair, d.AssetType, ws.GetName(), "websocket", err) - case wshandler.KlineData: - // Websocket Kline Data - if Bot.Settings.Verbose { - log.Infof(log.WebsocketMgr, "%s websocket %s %s kline updated %+v\n", - ws.GetName(), - FormatCurrency(d.Pair), - d.AssetType, - d) - } - case wshandler.WebsocketOrderbookUpdate: - // Websocket Orderbook Data - result := data.(wshandler.WebsocketOrderbookUpdate) - if Bot.Settings.EnableExchangeSyncManager && Bot.ExchangeCurrencyPairManager != nil { - Bot.ExchangeCurrencyPairManager.update(ws.GetName(), - result.Pair, - result.Asset, - SyncItemOrderbook, - nil) - } - - if Bot.Settings.Verbose { - log.Infof(log.WebsocketMgr, - "%s websocket %s %s orderbook updated\n", - ws.GetName(), - FormatCurrency(result.Pair), - d.Asset) - } - default: - if Bot.Settings.Verbose { - log.Warnf(log.WebsocketMgr, - "%s websocket Unknown type: %+v\n", - ws.GetName(), - d) - } + err := WebsocketDataHandler(ws.GetName(), data) + if err != nil { + log.Error(log.WebsocketMgr, err) } } } } + +// WebsocketDataHandler is a central point for exchange websocket implementations to send +// processed data. WebsocketDataHandler will then pass that to an appropriate handler +func WebsocketDataHandler(exchName string, data interface{}) error { + if data == nil { + return fmt.Errorf("routines.go - exchange %s nil data sent to websocket", + exchName) + } + switch d := data.(type) { + case string: + log.Info(log.WebsocketMgr, d) + case error: + return fmt.Errorf("routines.go exchange %s websocket error - %s", exchName, data) + case wshandler.TradeData: + if Bot.Settings.Verbose { + log.Infof(log.WebsocketMgr, "%s websocket %s %s trade updated %+v", + exchName, + FormatCurrency(d.CurrencyPair), + d.AssetType, + d) + } + case wshandler.FundingData: + if Bot.Settings.Verbose { + log.Infof(log.WebsocketMgr, "%s websocket %s %s funding updated %+v", + exchName, + FormatCurrency(d.CurrencyPair), + d.AssetType, + d) + } + case *ticker.Price: + if Bot.Settings.EnableExchangeSyncManager && Bot.ExchangeCurrencyPairManager != nil { + Bot.ExchangeCurrencyPairManager.update(exchName, + d.Pair, + d.AssetType, + SyncItemTicker, + nil) + } + err := ticker.ProcessTicker(exchName, d, d.AssetType) + printTickerSummary(d, d.Pair, d.AssetType, exchName, "websocket", err) + case wshandler.KlineData: + if Bot.Settings.Verbose { + log.Infof(log.WebsocketMgr, "%s websocket %s %s kline updated %+v", + exchName, + FormatCurrency(d.Pair), + d.AssetType, + d) + } + case wshandler.WebsocketOrderbookUpdate: + if Bot.Settings.EnableExchangeSyncManager && Bot.ExchangeCurrencyPairManager != nil { + Bot.ExchangeCurrencyPairManager.update(exchName, + d.Pair, + d.Asset, + SyncItemOrderbook, + nil) + } + + if Bot.Settings.Verbose { + log.Infof(log.WebsocketMgr, + "%s websocket %s %s orderbook updated", + exchName, + FormatCurrency(d.Pair), + d.Asset) + } + case *order.Detail: + if !Bot.OrderManager.orderStore.exists(d) { + err := Bot.OrderManager.orderStore.Add(d) + if err != nil { + return err + } + } else { + od, err := Bot.OrderManager.orderStore.GetByExchangeAndID(d.Exchange, d.ID) + if err != nil { + return err + } + od.UpdateOrderFromDetail(d) + } + case *order.Cancel: + return Bot.OrderManager.Cancel(d) + case *order.Modify: + od, err := Bot.OrderManager.orderStore.GetByExchangeAndID(d.Exchange, d.ID) + if err != nil { + return err + } + od.UpdateOrderFromModify(d) + case order.ClassificationError: + return errors.New(d.Error()) + case wshandler.UnhandledMessageWarning: + log.Warn(log.WebsocketMgr, d.Message) + default: + if Bot.Settings.Verbose { + log.Warnf(log.WebsocketMgr, + "%s websocket Unknown type: %+v", + exchName, + d) + } + } + return nil +} diff --git a/engine/routines_test.go b/engine/routines_test.go new file mode 100644 index 00000000..ac200633 --- /dev/null +++ b/engine/routines_test.go @@ -0,0 +1,127 @@ +package engine + +import ( + "errors" + "testing" + "time" + + "github.com/thrasher-corp/gocryptotrader/exchanges/order" + "github.com/thrasher-corp/gocryptotrader/exchanges/sharedtestvalues" + "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" + "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" +) + +func TestWebsocketDataHandlerProcess(t *testing.T) { + ws := wshandler.New() + err := ws.Setup(&wshandler.WebsocketSetup{Enabled: true}) + if err != nil { + t.Error(err) + } + ws.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride() + go WebsocketDataReceiver(ws) + ws.DataHandler <- "string" + time.Sleep(time.Second) + close(shutdowner) +} + +func TestHandleData(t *testing.T) { + OrdersSetup(t) + var exchName = "exch" + var orderID = "testOrder.Detail" + err := WebsocketDataHandler(exchName, errors.New("error")) + if err == nil { + t.Error("Error not handled correctly") + } + err = WebsocketDataHandler(exchName, nil) + if err == nil { + t.Error("Expected nil data error") + } + err = WebsocketDataHandler(exchName, wshandler.TradeData{}) + if err != nil { + t.Error(err) + } + err = WebsocketDataHandler(exchName, wshandler.FundingData{}) + if err != nil { + t.Error(err) + } + err = WebsocketDataHandler(exchName, &ticker.Price{}) + if err != nil { + t.Error(err) + } + err = WebsocketDataHandler(exchName, wshandler.KlineData{}) + if err != nil { + t.Error(err) + } + err = WebsocketDataHandler(exchName, wshandler.WebsocketOrderbookUpdate{}) + if err != nil { + t.Error(err) + } + origOrder := &order.Detail{ + Exchange: fakePassExchange, + ID: orderID, + Amount: 1337, + Price: 1337, + } + err = WebsocketDataHandler(exchName, origOrder) + if err != nil { + t.Error(err) + } + // Send it again since it exists now + err = WebsocketDataHandler(exchName, &order.Detail{ + Exchange: fakePassExchange, + ID: orderID, + Amount: 1338, + }) + if err != nil { + t.Error(err) + } + if origOrder.Amount != 1338 { + t.Error("Bad pipeline") + } + + err = WebsocketDataHandler(exchName, &order.Modify{ + Exchange: fakePassExchange, + ID: orderID, + Status: order.Active, + }) + if err != nil { + t.Error(err) + } + if origOrder.Status != order.Active { + t.Error("Expected order to be modified to Active") + } + + err = WebsocketDataHandler(exchName, &order.Cancel{ + Exchange: fakePassExchange, + ID: orderID, + }) + if err != nil { + t.Error(err) + } + if origOrder.Status != order.Cancelled { + t.Error("Expected order status to be cancelled") + } + // Send some gibberish + err = WebsocketDataHandler(exchName, order.Stop) + if err != nil { + t.Error(err) + } + + err = WebsocketDataHandler(exchName, wshandler.UnhandledMessageWarning{Message: "there's an issue here's a tissue"}) + if err != nil { + t.Error(err) + } + + classificationError := order.ClassificationError{ + Exchange: "test", + OrderID: "one", + Err: errors.New("lol"), + } + err = WebsocketDataHandler(exchName, classificationError) + if err == nil { + t.Error("Expected error") + } + if err.Error() != classificationError.Error() { + t.Errorf("Problem formatting error. Expected %v Received %v", classificationError.Error(), err.Error()) + } +} diff --git a/engine/rpcserver.go b/engine/rpcserver.go index 8a7ddeca..cde8b4bc 100644 --- a/engine/rpcserver.go +++ b/engine/rpcserver.go @@ -718,7 +718,7 @@ func (s *RPCServer) GetOrders(ctx context.Context, r *gctrpc.GetOrdersRequest) ( } resp, err := exch.GetActiveOrders(&order.GetOrdersRequest{ - Currencies: []currency.Pair{ + Pairs: []currency.Pair{ currency.NewPairWithDelimiter(r.Pair.Base, r.Pair.Quote, r.Pair.Delimiter), }, @@ -732,12 +732,12 @@ func (s *RPCServer) GetOrders(ctx context.Context, r *gctrpc.GetOrdersRequest) ( orders = append(orders, &gctrpc.OrderDetails{ Exchange: r.Exchange, Id: resp[x].ID, - BaseCurrency: resp[x].CurrencyPair.Base.String(), - QuoteCurrency: resp[x].CurrencyPair.Quote.String(), + BaseCurrency: resp[x].Pair.Base.String(), + QuoteCurrency: resp[x].Pair.Quote.String(), AssetType: asset.Spot.String(), - OrderType: resp[x].OrderType.String(), - OrderSide: resp[x].OrderSide.String(), - CreationTime: resp[x].OrderDate.Unix(), + OrderType: resp[x].Type.String(), + OrderSide: resp[x].Side.String(), + CreationTime: resp[x].Date.Unix(), Status: resp[x].Status.String(), Price: resp[x].Price, Amount: resp[x].Amount, @@ -761,18 +761,17 @@ func (s *RPCServer) SubmitOrder(ctx context.Context, r *gctrpc.SubmitOrderReques } p := currency.NewPairFromStrings(r.Pair.Base, r.Pair.Quote) - submission := &order.Submit{ - Pair: p, - OrderSide: order.Side(r.Side), - OrderType: order.Type(r.OrderType), - Amount: r.Amount, - Price: r.Price, - ClientID: r.ClientId, - } - result, err := exch.SubmitOrder(submission) + resp, err := Bot.OrderManager.Submit(&order.Submit{ + Pair: p, + Side: order.Side(r.Side), + Type: order.Type(r.OrderType), + Amount: r.Amount, + Price: r.Price, + ClientID: r.ClientId, + }) return &gctrpc.SubmitOrderResponse{ - OrderId: result.OrderID, - OrderPlaced: result.IsOrderPlaced, + OrderId: resp.OrderID, + OrderPlaced: resp.IsOrderPlaced, }, err } @@ -860,7 +859,7 @@ func (s *RPCServer) CancelOrder(ctx context.Context, r *gctrpc.CancelOrderReques err := exch.CancelOrder(&order.Cancel{ AccountID: r.AccountId, - OrderID: r.OrderId, + ID: r.OrderId, Side: order.Side(r.Side), WalletAddress: r.WalletAddress, }) @@ -1082,12 +1081,12 @@ func (s *RPCServer) WithdrawalEventsByExchange(ctx context.Context, r *gctrpc.Wi // WithdrawalEventsByDate returns previous withdrawal request details by exchange func (s *RPCServer) WithdrawalEventsByDate(ctx context.Context, r *gctrpc.WithdrawalEventsByDateRequest) (*gctrpc.WithdrawalEventsByExchangeResponse, error) { - UTCStartTime, err := time.Parse(audit.TableTimeFormat, r.Start) + UTCStartTime, err := time.Parse(common.SimpleTimeFormat, r.Start) if err != nil { return nil, err } - UTCSEndTime, err := time.Parse(audit.TableTimeFormat, r.End) + UTCSEndTime, err := time.Parse(common.SimpleTimeFormat, r.End) if err != nil { return nil, err } @@ -1423,12 +1422,12 @@ func (s *RPCServer) GetExchangeTickerStream(r *gctrpc.GetExchangeTickerStreamReq // GetAuditEvent returns matching audit events from database func (s *RPCServer) GetAuditEvent(ctx context.Context, r *gctrpc.GetAuditEventRequest) (*gctrpc.GetAuditEventResponse, error) { - UTCStartTime, err := time.Parse(audit.TableTimeFormat, r.StartDate) + UTCStartTime, err := time.Parse(common.SimpleTimeFormat, r.StartDate) if err != nil { return nil, err } - UTCSEndTime, err := time.Parse(audit.TableTimeFormat, r.EndDate) + UTCSEndTime, err := time.Parse(common.SimpleTimeFormat, r.EndDate) if err != nil { return nil, err } @@ -1449,7 +1448,7 @@ func (s *RPCServer) GetAuditEvent(ctx context.Context, r *gctrpc.GetAuditEventRe Type: v[x].Type, Identifier: v[x].Identifier, Message: v[x].Message, - Timestamp: v[x].CreatedAt.In(loc).Format(audit.TableTimeFormat), + Timestamp: v[x].CreatedAt.In(loc).Format(common.SimpleTimeFormat), } resp.Events = append(resp.Events, tempEvent) diff --git a/engine/syncer.go b/engine/syncer.go index 86a456d8..5ba892da 100644 --- a/engine/syncer.go +++ b/engine/syncer.go @@ -505,7 +505,7 @@ func (e *ExchangeCurrencyPairSyncer) Start() { } if !ws.IsConnected() && !ws.IsConnecting() { - go WebsocketDataHandler(ws) + go WebsocketDataReceiver(ws) err = ws.Connect() if err != nil { diff --git a/engine/withdraw_test.go b/engine/withdraw_test.go index 9829d495..a67e777b 100644 --- a/engine/withdraw_test.go +++ b/engine/withdraw_test.go @@ -14,7 +14,6 @@ import ( ) const ( - exchangeName = "BTC Markets" bankAccountID = "test-bank-01" ) @@ -30,14 +29,6 @@ var ( } ) -func setupEngine() (err error) { - Bot, err = NewFromSettings(&settings) - if err != nil { - return err - } - return Bot.Start() -} - func cleanup() { err := os.RemoveAll(settings.DataDir) if err != nil { @@ -46,11 +37,7 @@ func cleanup() { } func TestSubmitWithdrawal(t *testing.T) { - err := setupEngine() - if err != nil { - t.Fatal(err) - } - + SetupTestHelpers(t) banking.Accounts = append(banking.Accounts, banking.Account{ Enabled: true, @@ -66,7 +53,7 @@ func TestSubmitWithdrawal(t *testing.T) { SWIFTCode: "91272837", IBAN: "98218738671897", SupportedCurrencies: "AUD,USD", - SupportedExchanges: exchangeName, + SupportedExchanges: testExchange, }, ) @@ -75,9 +62,9 @@ func TestSubmitWithdrawal(t *testing.T) { t.Fatal(err) } req := &withdraw.Request{ - Exchange: exchangeName, + Exchange: testExchange, Currency: currency.AUD, - Description: exchangeName, + Description: testExchange, Amount: 1.0, Type: 1, Fiat: &withdraw.FiatRequest{ @@ -85,12 +72,12 @@ func TestSubmitWithdrawal(t *testing.T) { }, } - _, err = SubmitWithdrawal(exchangeName, req) + _, err = SubmitWithdrawal(testExchange, req) if err != nil { t.Fatal(err) } - _, err = SubmitWithdrawal(exchangeName, nil) + _, err = SubmitWithdrawal(testExchange, nil) if err != nil { if err.Error() != withdraw.ErrRequestCannotBeNil.Error() { t.Fatal(err) @@ -122,21 +109,21 @@ func TestWithdrawEventByID(t *testing.T) { } func TestWithdrawalEventByExchange(t *testing.T) { - _, err := WithdrawalEventByExchange(exchangeName, 1) + _, err := WithdrawalEventByExchange(testExchange, 1) if err == nil { t.Fatal(err) } } func TestWithdrawEventByDate(t *testing.T) { - _, err := WithdrawEventByDate(exchangeName, time.Now(), time.Now(), 1) + _, err := WithdrawEventByDate(testExchange, time.Now(), time.Now(), 1) if err == nil { t.Fatal(err) } } func TestWithdrawalEventByExchangeID(t *testing.T) { - _, err := WithdrawalEventByExchangeID(exchangeName, exchangeName) + _, err := WithdrawalEventByExchangeID(testExchange, testExchange) if err == nil { t.Fatal(err) } diff --git a/exchanges/alphapoint/alphapoint_test.go b/exchanges/alphapoint/alphapoint_test.go index bed1435a..053d7dba 100644 --- a/exchanges/alphapoint/alphapoint_test.go +++ b/exchanges/alphapoint/alphapoint_test.go @@ -439,7 +439,7 @@ func TestFormatWithdrawPermissions(t *testing.T) { func TestGetActiveOrders(t *testing.T) { t.Parallel() var getOrdersRequest = order.GetOrdersRequest{ - OrderType: order.AnyType, + Type: order.AnyType, } _, err := a.GetActiveOrders(&getOrdersRequest) @@ -453,7 +453,7 @@ func TestGetActiveOrders(t *testing.T) { func TestGetOrderHistory(t *testing.T) { t.Parallel() var getOrdersRequest = order.GetOrdersRequest{ - OrderType: order.AnyType, + Type: order.AnyType, } _, err := a.GetOrderHistory(&getOrdersRequest) @@ -479,11 +479,11 @@ func TestSubmitOrder(t *testing.T) { Base: currency.BTC, Quote: currency.USD, }, - OrderSide: order.Buy, - OrderType: order.Limit, - Price: 1, - Amount: 1, - ClientID: "meowOrder", + Side: order.Buy, + Type: order.Limit, + Price: 1, + Amount: 1, + ClientID: "meowOrder", } response, err := a.SubmitOrder(orderSubmission) @@ -507,10 +507,10 @@ func TestCancelExchangeOrder(t *testing.T) { currencyPair := currency.NewPair(currency.BTC, currency.LTC) var orderCancellation = &order.Cancel{ - OrderID: "1", + ID: "1", WalletAddress: core.BitcoinDonationAddress, AccountID: "1", - CurrencyPair: currencyPair, + Pair: currencyPair, } err := a.CancelOrder(orderCancellation) @@ -530,10 +530,10 @@ func TestCancelAllExchangeOrders(t *testing.T) { currencyPair := currency.NewPair(currency.BTC, currency.LTC) var orderCancellation = &order.Cancel{ - OrderID: "1", + ID: "1", WalletAddress: core.BitcoinDonationAddress, AccountID: "1", - CurrencyPair: currencyPair, + Pair: currencyPair, } resp, err := a.CancelAllOrders(orderCancellation) diff --git a/exchanges/alphapoint/alphapoint_wrapper.go b/exchanges/alphapoint/alphapoint_wrapper.go index 4eace9c8..15899f18 100644 --- a/exchanges/alphapoint/alphapoint_wrapper.go +++ b/exchanges/alphapoint/alphapoint_wrapper.go @@ -225,8 +225,8 @@ func (a *Alphapoint) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) } response, err := a.CreateOrder(s.Pair.String(), - s.OrderSide.String(), - s.OrderSide.String(), + s.Side.String(), + s.Type.String(), s.Amount, s.Price) if err != nil { @@ -235,7 +235,7 @@ func (a *Alphapoint) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) if response > 0 { submitOrderResponse.OrderID = strconv.FormatInt(response, 10) } - if s.OrderType == order.Market { + if s.Type == order.Market { submitOrderResponse.FullyMatched = true } submitOrderResponse.IsOrderPlaced = true @@ -251,7 +251,7 @@ func (a *Alphapoint) ModifyOrder(_ *order.Modify) (string, error) { // CancelOrder cancels an order by its corresponding ID number func (a *Alphapoint) CancelOrder(order *order.Cancel) error { - orderIDInt, err := strconv.ParseInt(order.OrderID, 10, 64) + orderIDInt, err := strconv.ParseInt(order.ID, 10, 64) if err != nil { return err } @@ -348,19 +348,19 @@ func (a *Alphapoint) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detai RemainingAmount: resp[x].OpenOrders[y].QtyRemaining, } - orderDetail.OrderSide = orderSideMap[resp[x].OpenOrders[y].Side] - orderDetail.OrderDate = time.Unix(resp[x].OpenOrders[y].ReceiveTime, 0) - orderDetail.OrderType = orderTypeMap[resp[x].OpenOrders[y].OrderType] - if orderDetail.OrderType == "" { - orderDetail.OrderType = order.Unknown + orderDetail.Side = orderSideMap[resp[x].OpenOrders[y].Side] + orderDetail.Date = time.Unix(resp[x].OpenOrders[y].ReceiveTime, 0) + orderDetail.Type = orderTypeMap[resp[x].OpenOrders[y].OrderType] + if orderDetail.Type == "" { + orderDetail.Type = order.UnknownType } orders = append(orders, orderDetail) } } - order.FilterOrdersByType(&orders, req.OrderType) - order.FilterOrdersBySide(&orders, req.OrderSide) + order.FilterOrdersByType(&orders, req.Type) + order.FilterOrdersBySide(&orders, req.Side) order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) return orders, nil } @@ -390,19 +390,19 @@ func (a *Alphapoint) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detai RemainingAmount: resp[x].OpenOrders[y].QtyRemaining, } - orderDetail.OrderSide = orderSideMap[resp[x].OpenOrders[y].Side] - orderDetail.OrderDate = time.Unix(resp[x].OpenOrders[y].ReceiveTime, 0) - orderDetail.OrderType = orderTypeMap[resp[x].OpenOrders[y].OrderType] - if orderDetail.OrderType == "" { - orderDetail.OrderType = order.Unknown + orderDetail.Side = orderSideMap[resp[x].OpenOrders[y].Side] + orderDetail.Date = time.Unix(resp[x].OpenOrders[y].ReceiveTime, 0) + orderDetail.Type = orderTypeMap[resp[x].OpenOrders[y].OrderType] + if orderDetail.Type == "" { + orderDetail.Type = order.UnknownType } orders = append(orders, orderDetail) } } - order.FilterOrdersByType(&orders, req.OrderType) - order.FilterOrdersBySide(&orders, req.OrderSide) + order.FilterOrdersByType(&orders, req.Type) + order.FilterOrdersBySide(&orders, req.Side) order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) return orders, nil } diff --git a/exchanges/binance/binance.go b/exchanges/binance/binance.go index 5b795e2c..f1817c32 100644 --- a/exchanges/binance/binance.go +++ b/exchanges/binance/binance.go @@ -26,17 +26,18 @@ const ( apiURL = "https://api.binance.com" // Public endpoints - exchangeInfo = "/api/v1/exchangeInfo" - orderBookDepth = "/api/v1/depth" - recentTrades = "/api/v1/trades" - historicalTrades = "/api/v1/historicalTrades" - aggregatedTrades = "/api/v1/aggTrades" - candleStick = "/api/v1/klines" - averagePrice = "/api/v3/avgPrice" - priceChange = "/api/v1/ticker/24hr" - symbolPrice = "/api/v3/ticker/price" - bestPrice = "/api/v3/ticker/bookTicker" - accountInfo = "/api/v3/account" + exchangeInfo = "/api/v1/exchangeInfo" + orderBookDepth = "/api/v1/depth" + recentTrades = "/api/v1/trades" + historicalTrades = "/api/v1/historicalTrades" + aggregatedTrades = "/api/v1/aggTrades" + candleStick = "/api/v1/klines" + averagePrice = "/api/v3/avgPrice" + priceChange = "/api/v1/ticker/24hr" + symbolPrice = "/api/v3/ticker/price" + bestPrice = "/api/v3/ticker/bookTicker" + accountInfo = "/api/v3/account" + userAccountStream = "/api/v3/userDataStream" // Authenticated endpoints newOrderTest = "/api/v3/order/test" @@ -683,3 +684,51 @@ func (b *Binance) GetDepositAddressForCurrency(currency string) (string, error) return resp.Address, b.SendAuthHTTPRequest(http.MethodGet, path, params, request.Unset, &resp) } + +// GetWsAuthStreamKey will retrieve a key to use for authorised WS streaming +func (b *Binance) GetWsAuthStreamKey() (string, error) { + var resp UserAccountStream + path := b.API.Endpoints.URL + userAccountStream + headers := make(map[string]string) + headers["X-MBX-APIKEY"] = b.API.Credentials.Key + err := b.SendPayload(&request.Item{ + Method: http.MethodPost, + Path: path, + Headers: headers, + Body: bytes.NewBuffer(nil), + Result: &resp, + AuthRequest: true, + Verbose: b.Verbose, + HTTPDebugging: b.HTTPDebugging, + HTTPRecording: b.HTTPRecording, + }) + if err != nil { + return "", err + } + return resp.ListenKey, nil +} + +// MaintainWsAuthStreamKey will keep the key alive +func (b *Binance) MaintainWsAuthStreamKey() error { + var err error + if listenKey == "" { + listenKey, err = b.GetWsAuthStreamKey() + return err + } + path := b.API.Endpoints.URL + userAccountStream + params := url.Values{} + params.Set("listenKey", listenKey) + path = common.EncodeURLValues(path, params) + headers := make(map[string]string) + headers["X-MBX-APIKEY"] = b.API.Credentials.Key + return b.SendPayload(&request.Item{ + Method: http.MethodPut, + Path: path, + Headers: headers, + Body: bytes.NewBuffer(nil), + AuthRequest: true, + Verbose: b.Verbose, + HTTPDebugging: b.HTTPDebugging, + HTTPRecording: b.HTTPRecording, + }) +} diff --git a/exchanges/binance/binance_live_test.go b/exchanges/binance/binance_live_test.go index d9745102..643af815 100644 --- a/exchanges/binance/binance_live_test.go +++ b/exchanges/binance/binance_live_test.go @@ -33,6 +33,7 @@ func TestMain(m *testing.M) { if err != nil { log.Fatal("Binance setup error", err) } + b.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride() log.Printf(sharedtestvalues.LiveTesting, b.Name, b.API.Endpoints.URL) os.Exit(m.Run()) } diff --git a/exchanges/binance/binance_mock_test.go b/exchanges/binance/binance_mock_test.go index 28160f03..55974656 100644 --- a/exchanges/binance/binance_mock_test.go +++ b/exchanges/binance/binance_mock_test.go @@ -45,7 +45,7 @@ func TestMain(m *testing.M) { b.HTTPClient = newClient b.API.Endpoints.URL = serverDetails - + b.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride() log.Printf(sharedtestvalues.MockTesting, b.Name, b.API.Endpoints.URL) os.Exit(m.Run()) } diff --git a/exchanges/binance/binance_test.go b/exchanges/binance/binance_test.go index 5263a100..f15f8100 100644 --- a/exchanges/binance/binance_test.go +++ b/exchanges/binance/binance_test.go @@ -302,14 +302,14 @@ func TestGetActiveOrders(t *testing.T) { t.Parallel() var getOrdersRequest = order.GetOrdersRequest{ - OrderType: order.AnyType, + Type: order.AnyType, } _, err := b.GetActiveOrders(&getOrdersRequest) if err == nil { t.Error("Expected: 'At least one currency is required to fetch order history'. received nil") } - getOrdersRequest.Currencies = []currency.Pair{ + getOrdersRequest.Pairs = []currency.Pair{ currency.NewPair(currency.LTC, currency.BTC), } @@ -328,7 +328,7 @@ func TestGetOrderHistory(t *testing.T) { t.Parallel() var getOrdersRequest = order.GetOrdersRequest{ - OrderType: order.AnyType, + Type: order.AnyType, } _, err := b.GetOrderHistory(&getOrdersRequest) @@ -336,7 +336,7 @@ func TestGetOrderHistory(t *testing.T) { t.Error("Expected: 'At least one currency is required to fetch order history'. received nil") } - getOrdersRequest.Currencies = []currency.Pair{ + getOrdersRequest.Pairs = []currency.Pair{ currency.NewPair(currency.LTC, currency.BTC)} @@ -367,11 +367,11 @@ func TestSubmitOrder(t *testing.T) { Base: currency.LTC, Quote: currency.BTC, }, - OrderSide: order.Buy, - OrderType: order.Limit, - Price: 1, - Amount: 1000000000, - ClientID: "meowOrder", + Side: order.Buy, + Type: order.Limit, + Price: 1, + Amount: 1000000000, + ClientID: "meowOrder", } _, err := b.SubmitOrder(orderSubmission) @@ -392,10 +392,10 @@ func TestCancelExchangeOrder(t *testing.T) { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } var orderCancellation = &order.Cancel{ - OrderID: "1", + ID: "1", WalletAddress: core.BitcoinDonationAddress, AccountID: "1", - CurrencyPair: currency.NewPair(currency.LTC, currency.BTC), + Pair: currency.NewPair(currency.LTC, currency.BTC), } err := b.CancelOrder(orderCancellation) @@ -416,10 +416,10 @@ func TestCancelAllExchangeOrders(t *testing.T) { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } var orderCancellation = &order.Cancel{ - OrderID: "1", + ID: "1", WalletAddress: core.BitcoinDonationAddress, AccountID: "1", - CurrencyPair: currency.NewPair(currency.LTC, currency.BTC), + Pair: currency.NewPair(currency.LTC, currency.BTC), } _, err := b.CancelAllOrders(orderCancellation) @@ -514,3 +514,279 @@ func TestGetDepositAddress(t *testing.T) { t.Error("Mock GetDepositAddress() error", err) } } + +func TestWSSubscriptionHandling(t *testing.T) { + t.Parallel() + pressXToJSON := []byte(`{ + "method": "SUBSCRIBE", + "params": [ + "btcusdt@aggTrade", + "btcusdt@depth" + ], + "id": 1 +}`) + err := b.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWSUnsubscriptionHandling(t *testing.T) { + pressXToJSON := []byte(`{ + "method": "UNSUBSCRIBE", + "params": [ + "btcusdt@depth" + ], + "id": 312 +}`) + err := b.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsOrderUpdateHandling(t *testing.T) { + t.Parallel() + pressXToJSON := []byte(`{ + "e": "executionReport", + "E": 1499405658658, + "s": "BTCUSDT", + "c": "mUvoqJxFIILMdfAW5iGSOW", + "S": "BUY", + "o": "LIMIT", + "f": "GTC", + "q": "1.00000000", + "p": "0.10264410", + "P": "0.00000000", + "F": "0.00000000", + "g": -1, + "C": null, + "x": "NEW", + "X": "NEW", + "r": "NONE", + "i": 4293153, + "l": "0.00000000", + "z": "0.00000000", + "L": "0.00000000", + "n": "0", + "N": null, + "T": 1499405658657, + "t": -1, + "I": 8641984, + "w": true, + "m": false, + "M": false, + "O": 1499405658657, + "Z": "0.00000000", + "Y": "0.00000000", + "Q": "0.00000000" + }`) + err := b.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsOutboundAccountPosition(t *testing.T) { + t.Parallel() + pressXToJSON := []byte(`{ + "e": "outboundAccountPosition", + "E": 1564034571105, + "u": 1564034571073, + "B": [ + { + "a": "ETH", + "f": "10000.000000", + "l": "0.000000" + } + ] +}`) + err := b.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsTickerUpdate(t *testing.T) { + t.Parallel() + pressXToJSON := []byte(`{"stream":"btcusdt@ticker","data":{"e":"24hrTicker","E":1580254809477,"s":"BTCUSDT","p":"420.97000000","P":"4.720","w":"9058.27981278","x":"8917.98000000","c":"9338.96000000","Q":"0.17246300","b":"9338.03000000","B":"0.18234600","a":"9339.70000000","A":"0.14097600","o":"8917.99000000","h":"9373.19000000","l":"8862.40000000","v":"72229.53692000","q":"654275356.16896672","O":1580168409456,"C":1580254809456,"F":235294268,"L":235894703,"n":600436}}`) + err := b.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsKlineUpdate(t *testing.T) { + t.Parallel() + pressXToJSON := []byte(`{"stream":"btcusdt@kline_1m","data":{ + "e": "kline", + "E": 123456789, + "s": "BNBBTC", + "k": { + "t": 123400000, + "T": 123460000, + "s": "BNBBTC", + "i": "1m", + "f": 100, + "L": 200, + "o": "0.0010", + "c": "0.0020", + "h": "0.0025", + "l": "0.0015", + "v": "1000", + "n": 100, + "x": false, + "q": "1.0000", + "V": "500", + "Q": "0.500", + "B": "123456" + } + }}`) + err := b.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsTradeUpdate(t *testing.T) { + t.Parallel() + pressXToJSON := []byte(`{"stream":"btcusdt@trade","data":{ + "e": "trade", + "E": 123456789, + "s": "BNBBTC", + "t": 12345, + "p": "0.001", + "q": "100", + "b": 88, + "a": 50, + "T": 123456785, + "m": true, + "M": true + }}`) + err := b.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsDepthUpdate(t *testing.T) { + t.Parallel() + pressXToJSON := []byte(`{"stream":"btcusdt@depth","data":{ + "e": "depthUpdate", + "E": 123456789, + "s": "BTCUSDT", + "U": 157, + "u": 160, + "b": [ + [ + "0.0024", + "10" + ] + ], + "a": [ + [ + "0.0026", + "100" + ] + ] + }}`) + + err := b.wsHandleData(pressXToJSON) + if err.Error() != "Binance - UpdateLocalCache error: ob.Base could not be found for Exchange Binance CurrencyPair: BTC-USDT AssetType: spot" { + t.Error(err) + } +} + +func TestWsBalanceUpdate(t *testing.T) { + t.Parallel() + pressXToJSON := []byte(`{ + "e": "balanceUpdate", + "E": 1573200697110, + "a": "BTC", + "d": "100.00000000", + "T": 1573200697068 +}`) + err := b.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsOCO(t *testing.T) { + t.Parallel() + pressXToJSON := []byte(`{ + "e": "listStatus", + "E": 1564035303637, + "s": "ETHBTC", + "g": 2, + "c": "OCO", + "l": "EXEC_STARTED", + "L": "EXECUTING", + "r": "NONE", + "C": "F4QN4G8DlFATFlIUQ0cjdD", + "T": 1564035303625, + "O": [ + { + "s": "ETHBTC", + "i": 17, + "c": "AJYsMjErWJesZvqlJCTUgL" + }, + { + "s": "ETHBTC", + "i": 18, + "c": "bfYPSQdLoqAJeNrOr9adzq" + } + ] +}`) + err := b.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestGetWsAuthStreamKey(t *testing.T) { + key, err := b.GetWsAuthStreamKey() + switch { + case mockTests && err != nil, + !mockTests && areTestAPIKeysSet() && err != nil: + t.Fatal(err) + case !mockTests && !areTestAPIKeysSet() && err == nil: + t.Fatal("Expected error") + } + + if key == "" { + t.Error("Expected key") + } +} + +func TestMaintainWsAuthStreamKey(t *testing.T) { + err := b.MaintainWsAuthStreamKey() + switch { + case mockTests && err != nil, + !mockTests && areTestAPIKeysSet() && err != nil: + t.Fatal(err) + case !mockTests && !areTestAPIKeysSet() && err == nil: + t.Fatal("Expected error") + } +} + +func TestExecutionTypeToOrderStatus(t *testing.T) { + type TestCases struct { + Case string + Result order.Status + } + testCases := []TestCases{ + {Case: "NEW", Result: order.New}, + {Case: "CANCELLED", Result: order.Cancelled}, + {Case: "REJECTED", Result: order.Rejected}, + {Case: "TRADE", Result: order.PartiallyFilled}, + {Case: "EXPIRED", Result: order.Expired}, + {Case: "LOL", Result: order.UnknownStatus}, + } + for i := range testCases { + result, _ := stringToOrderStatus(testCases[i].Case) + if result != testCases[i].Result { + t.Errorf("Exepcted: %v, received: %v", testCases[i].Result, result) + } + } +} diff --git a/exchanges/binance/binance_types.go b/exchanges/binance/binance_types.go index a60e4e0f..7277eb01 100644 --- a/exchanges/binance/binance_types.go +++ b/exchanges/binance/binance_types.go @@ -1,8 +1,6 @@ package binance import ( - "encoding/json" - "github.com/thrasher-corp/gocryptotrader/currency" ) @@ -119,12 +117,6 @@ type RecentTrade struct { IsBestMatch bool `json:"isBestMatch"` } -// MultiStreamData holds stream data -type MultiStreamData struct { - Stream string `json:"stream"` - Data json.RawMessage `json:"data"` -} - // TradeStream holds the trade stream data type TradeStream struct { EventType string `json:"e"` @@ -146,22 +138,22 @@ type KlineStream struct { EventTime int64 `json:"E"` Symbol string `json:"s"` Kline struct { - StartTime int64 `json:"t"` - CloseTime int64 `json:"T"` - Symbol string `json:"s"` - Interval string `json:"i"` - FirstTradeID int64 `json:"f"` - LastTradeID int64 `json:"L"` - OpenPrice string `json:"o"` - ClosePrice string `json:"c"` - HighPrice string `json:"h"` - LowPrice string `json:"l"` - Volume string `json:"v"` - NumberOfTrades int64 `json:"n"` - KlineClosed bool `json:"x"` - Quote string `json:"q"` - TakerBuyBaseAssetVolume string `json:"V"` - TakerBuyQuoteAssetVolume string `json:"Q"` + StartTime int64 `json:"t"` + CloseTime int64 `json:"T"` + Symbol string `json:"s"` + Interval string `json:"i"` + FirstTradeID int64 `json:"f"` + LastTradeID int64 `json:"L"` + OpenPrice float64 `json:"o,string"` + ClosePrice float64 `json:"c,string"` + HighPrice float64 `json:"h,string"` + LowPrice float64 `json:"l,string"` + Volume float64 `json:"v,string"` + NumberOfTrades int64 `json:"n"` + KlineClosed bool `json:"x"` + Quote float64 `json:"q,string"` + TakerBuyBaseAssetVolume float64 `json:"V,string"` + TakerBuyQuoteAssetVolume float64 `json:"Q,string"` } `json:"k"` } @@ -612,3 +604,97 @@ type WithdrawResponse struct { Msg string `json:"msg"` ID string `json:"id"` } + +// UserAccountStream contains a key to maintain an authorised +// websocket connection +type UserAccountStream struct { + ListenKey string `json:"listenKey"` +} + +type wsAccountInfo struct { + CanDeposit bool `json:"D"` + CanTrade bool `json:"T"` + CanWithdraw bool `json:"W"` + EventTime int64 `json:"E"` + LastUpdated int64 `json:"u"` + BuyerCommission float64 `json:"b"` + MakerCommission float64 `json:"m"` + SellerCommission float64 `json:"s"` + TakerCommission float64 `json:"t"` + EventType string `json:"e"` + Currencies []struct { + Asset string `json:"a"` + Available float64 `json:"f,string"` + Locked float64 `json:"l,string"` + } `json:"B"` +} + +type wsAccountPosition struct { + Currencies []struct { + Asset string `json:"a"` + Available float64 `json:"f,string"` + Locked float64 `json:"l,string"` + } `json:"B"` + EventTime int64 `json:"E"` + LastUpdated int64 `json:"u"` + EventType string `json:"e"` +} + +type wsBalanceUpdate struct { + EventTime int64 `json:"E"` + ClearTime int64 `json:"T"` + BalanceDelta float64 `json:"d,string"` + Asset string `json:"a"` + EventType string `json:"e"` +} + +type wsOrderUpdate struct { + ClientOrderID string `json:"C"` + EventTime int64 `json:"E"` + IcebergQuantity float64 `json:"F,string"` + LastExecutedPrice float64 `json:"L,string"` + CommissionAsset float64 `json:"N"` + OrderCreationTime int64 `json:"O"` + StopPrice float64 `json:"P,string"` + QuoteOrderQuantity float64 `json:"Q,string"` + Side string `json:"S"` + TransactionTime int64 `json:"T"` + OrderStatus string `json:"X"` + LastQuoteAssetTransactedQuantity float64 `json:"Y,string"` + CumulativeQuoteTransactedQuantity float64 `json:"Z,string"` + CancelledClientOrderID string `json:"c"` + EventType string `json:"e"` + TimeInForce string `json:"f"` + OrderListID int64 `json:"g"` + OrderID int64 `json:"i"` + LastExecutedQuantity float64 `json:"l,string"` + IsMaker bool `json:"m"` + Commission float64 `json:"n,string"` + OrderType string `json:"o"` + Price float64 `json:"p,string"` + Quantity float64 `json:"q,string"` + RejectionReason string `json:"r"` + Symbol string `json:"s"` + TradeID int64 `json:"t"` + IsOnOrderBook bool `json:"w"` + CurrentExecutionType string `json:"x"` + CumulativeFilledQuantity float64 `json:"z,string"` +} + +type wsListStauts struct { + ListClientOrderID string `json:"C"` + EventTime int64 `json:"E"` + ListOrderStatus string `json:"L"` + Orders []struct { + ClientOrderID string `json:"c"` + OrderID int64 `json:"i"` + Symbol string `json:"s"` + } `json:"O"` + TransactionTime int64 `json:"T"` + ContingencyType string `json:"c"` + EventType string `json:"e"` + OrderListID int64 `json:"g"` + ListStatusType string `json:"l"` + RejectionReason string `json:"r"` + Symbol string `json:"s"` +} diff --git a/exchanges/binance/binance_websocket.go b/exchanges/binance/binance_websocket.go index 74d4e2a8..8df68f6f 100644 --- a/exchanges/binance/binance_websocket.go +++ b/exchanges/binance/binance_websocket.go @@ -12,10 +12,12 @@ import ( "github.com/gorilla/websocket" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wsorderbook" + "github.com/thrasher-corp/gocryptotrader/log" ) const ( @@ -23,6 +25,8 @@ const ( pingDelay = time.Minute * 9 ) +var listenKey string + // WsConnect intiates a websocket connection func (b *Binance) WsConnect() error { if !b.Websocket.IsEnabled() || !b.IsEnabled() { @@ -31,6 +35,13 @@ func (b *Binance) WsConnect() error { var dialer websocket.Dialer var err error + if b.Websocket.CanUseAuthenticatedEndpoints() { + listenKey, err = b.GetWsAuthStreamKey() + if err != nil { + b.Websocket.SetCanUseAuthenticatedEndpoints(false) + log.Errorf(log.ExchangeSys, "%v unable to connect to authenticated Websocket. Error: %s", b.Name, err) + } + } pairs := b.GetEnabledPairs(asset.Spot).Strings() tick := strings.ToLower( @@ -55,6 +66,11 @@ func (b *Binance) WsConnect() error { kline + "/" + depth + if listenKey != "" { + wsurl += "/" + + listenKey + } + enabledPairs := b.GetEnabledPairs(asset.Spot) for i := range enabledPairs { err = b.SeedLocalCache(enabledPairs[i]) @@ -77,13 +93,36 @@ func (b *Binance) WsConnect() error { MessageType: websocket.PongMessage, Delay: pingDelay, }) - go b.WsHandleData() - + go b.wsReadData() + go b.KeepAuthKeyAlive() return nil } -// WsHandleData handles websocket data from WsReadData -func (b *Binance) WsHandleData() { +// KeepAuthKeyAlive will continuously send messages to +// keep the WS auth key active +func (b *Binance) KeepAuthKeyAlive() { + b.Websocket.Wg.Add(1) + defer func() { + b.Websocket.Wg.Done() + }() + ticks := time.NewTicker(time.Minute * 30) + for { + select { + case <-b.Websocket.ShutdownC: + ticks.Stop() + return + case <-ticks.C: + err := b.MaintainWsAuthStreamKey() + if err != nil { + b.Websocket.DataHandler <- err + log.Warnf(log.ExchangeSys, b.Name+" - Unable to renew auth websocket token, may experience shutdown") + } + } + } +} + +// wsReadData receives and passes on websocket messages for processing +func (b *Binance) wsReadData() { b.Websocket.Wg.Add(1) defer func() { b.Websocket.Wg.Done() @@ -94,144 +133,273 @@ func (b *Binance) WsHandleData() { return default: - read, err := b.WebsocketConn.ReadMessage() + resp, err := b.WebsocketConn.ReadMessage() if err != nil { b.Websocket.ReadMessageErrors <- err return } b.Websocket.TrafficAlert <- struct{}{} - var multiStreamData MultiStreamData - err = json.Unmarshal(read.Raw, &multiStreamData) + err = b.wsHandleData(resp.Raw) if err != nil { - b.Websocket.DataHandler <- fmt.Errorf("%v - Could not load multi stream data: %s", - b.Name, - read.Raw) - continue - } - streamType := strings.Split(multiStreamData.Stream, "@") - switch streamType[1] { - case "trade": - trade := TradeStream{} - err := json.Unmarshal(multiStreamData.Data, &trade) - if err != nil { - b.Websocket.DataHandler <- fmt.Errorf("%v - Could not unmarshal trade data: %s", - b.Name, - err) - continue - } - - price, err := strconv.ParseFloat(trade.Price, 64) - if err != nil { - b.Websocket.DataHandler <- fmt.Errorf("%v - price conversion error: %s", - b.Name, - err) - continue - } - - amount, err := strconv.ParseFloat(trade.Quantity, 64) - if err != nil { - b.Websocket.DataHandler <- fmt.Errorf("%v - amount conversion error: %s", - b.Name, - err) - continue - } - - b.Websocket.DataHandler <- wshandler.TradeData{ - CurrencyPair: currency.NewPairFromFormattedPairs(trade.Symbol, b.GetEnabledPairs(asset.Spot), - b.GetPairFormat(asset.Spot, true)), - Timestamp: time.Unix(0, trade.TimeStamp*int64(time.Millisecond)), - Price: price, - Amount: amount, - Exchange: b.Name, - AssetType: asset.Spot, - Side: trade.EventType, - } - continue - case "ticker": - t := TickerStream{} - err := json.Unmarshal(multiStreamData.Data, &t) - if err != nil { - b.Websocket.DataHandler <- fmt.Errorf("%v - Could not convert to a TickerStream structure %s", - b.Name, - err.Error()) - continue - } - - b.Websocket.DataHandler <- &ticker.Price{ - ExchangeName: b.Name, - Open: t.OpenPrice, - Close: t.ClosePrice, - Volume: t.TotalTradedVolume, - QuoteVolume: t.TotalTradedQuoteVolume, - High: t.HighPrice, - Low: t.LowPrice, - Bid: t.BestBidPrice, - Ask: t.BestAskPrice, - Last: t.LastPrice, - LastUpdated: time.Unix(0, t.EventTime*int64(time.Millisecond)), - AssetType: asset.Spot, - Pair: currency.NewPairFromFormattedPairs(t.Symbol, b.GetEnabledPairs(asset.Spot), - b.GetPairFormat(asset.Spot, true)), - } - - continue - case "kline_1m": - kline := KlineStream{} - err := json.Unmarshal(multiStreamData.Data, &kline) - if err != nil { - b.Websocket.DataHandler <- fmt.Errorf("%v - Could not convert to a KlineStream structure %s", - b.Name, - err) - continue - } - - var wsKline wshandler.KlineData - wsKline.Timestamp = time.Unix(0, kline.EventTime*int64(time.Millisecond)) - wsKline.Pair = currency.NewPairFromFormattedPairs(kline.Symbol, b.GetEnabledPairs(asset.Spot), - b.GetPairFormat(asset.Spot, true)) - wsKline.AssetType = asset.Spot - wsKline.Exchange = b.Name - wsKline.StartTime = time.Unix(0, kline.Kline.StartTime*int64(time.Millisecond)) - wsKline.CloseTime = time.Unix(0, kline.Kline.CloseTime*int64(time.Millisecond)) - wsKline.Interval = kline.Kline.Interval - wsKline.OpenPrice, _ = strconv.ParseFloat(kline.Kline.OpenPrice, 64) - wsKline.ClosePrice, _ = strconv.ParseFloat(kline.Kline.ClosePrice, 64) - wsKline.HighPrice, _ = strconv.ParseFloat(kline.Kline.HighPrice, 64) - wsKline.LowPrice, _ = strconv.ParseFloat(kline.Kline.LowPrice, 64) - wsKline.Volume, _ = strconv.ParseFloat(kline.Kline.Volume, 64) - b.Websocket.DataHandler <- wsKline - continue - case "depth": - depth := WebsocketDepthStream{} - err := json.Unmarshal(multiStreamData.Data, &depth) - if err != nil { - b.Websocket.DataHandler <- fmt.Errorf("%v - Could not convert to depthStream structure %s", - b.Name, - err) - continue - } - - err = b.UpdateLocalCache(&depth) - if err != nil { - b.Websocket.DataHandler <- fmt.Errorf("%v - UpdateLocalCache error: %s", - b.Name, - err) - continue - } - - currencyPair := currency.NewPairFromFormattedPairs(depth.Pair, b.GetEnabledPairs(asset.Spot), - b.GetPairFormat(asset.Spot, true)) - b.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{ - Pair: currencyPair, - Asset: asset.Spot, - Exchange: b.Name, - } - continue + b.Websocket.DataHandler <- err } } } } +func (b *Binance) wsHandleData(respRaw []byte) error { + var multiStreamData map[string]interface{} + err := json.Unmarshal(respRaw, &multiStreamData) + if err != nil { + return err + } + if method, ok := multiStreamData["method"].(string); ok { + // TODO handle subscription handling + if strings.EqualFold(method, "subscribe") { + return nil + } + if strings.EqualFold(method, "unsubscribe") { + return nil + } + } + if e, ok := multiStreamData["e"].(string); ok { + switch e { + case "outboundAccountInfo": + var data wsAccountInfo + err := json.Unmarshal(respRaw, &data) + if err != nil { + return fmt.Errorf("%v - Could not convert to outboundAccountInfo structure %s", + b.Name, + err) + } + b.Websocket.DataHandler <- data + case "outboundAccountPosition": + var data wsAccountPosition + err := json.Unmarshal(respRaw, &data) + if err != nil { + return fmt.Errorf("%v - Could not convert to outboundAccountPosition structure %s", + b.Name, + err) + } + b.Websocket.DataHandler <- data + case "balanceUpdate": + var data wsBalanceUpdate + err := json.Unmarshal(respRaw, &data) + if err != nil { + return fmt.Errorf("%v - Could not convert to balanceUpdate structure %s", + b.Name, + err) + } + b.Websocket.DataHandler <- data + case "executionReport": + var data wsOrderUpdate + err := json.Unmarshal(respRaw, &data) + if err != nil { + return fmt.Errorf("%v - Could not convert to executionReport structure %s", + b.Name, + err) + } + var orderID = strconv.FormatInt(data.OrderID, 10) + oType, err := order.StringToOrderType(data.OrderType) + if err != nil { + b.Websocket.DataHandler <- order.ClassificationError{ + Exchange: b.Name, + OrderID: orderID, + Err: err, + } + } + var oSide order.Side + oSide, err = order.StringToOrderSide(data.Side) + if err != nil { + b.Websocket.DataHandler <- order.ClassificationError{ + Exchange: b.Name, + OrderID: orderID, + Err: err, + } + } + var oStatus order.Status + oStatus, err = stringToOrderStatus(data.CurrentExecutionType) + if err != nil { + b.Websocket.DataHandler <- order.ClassificationError{ + Exchange: b.Name, + OrderID: orderID, + Err: err, + } + } + var p currency.Pair + var a asset.Item + p, a, err = b.GetRequestFormattedPairAndAssetType(data.Symbol) + if err != nil { + return err + } + b.Websocket.DataHandler <- &order.Detail{ + Price: data.Price, + Amount: data.Quantity, + ExecutedAmount: data.CumulativeFilledQuantity, + RemainingAmount: data.Quantity - data.CumulativeFilledQuantity, + Exchange: b.Name, + ID: orderID, + Type: oType, + Side: oSide, + Status: oStatus, + AssetType: a, + Date: time.Unix(0, data.OrderCreationTime*int64(time.Millisecond)), + Pair: p, + } + case "listStatus": + var data wsListStauts + err := json.Unmarshal(respRaw, &data) + if err != nil { + return fmt.Errorf("%v - Could not convert to listStatus structure %s", + b.Name, + err) + } + b.Websocket.DataHandler <- data + default: + b.Websocket.DataHandler <- wshandler.UnhandledMessageWarning{Message: b.Name + wshandler.UnhandledMessage + string(respRaw)} + return nil + } + } + if stream, ok := multiStreamData["stream"].(string); ok { + streamType := strings.Split(stream, "@") + if len(streamType) > 1 { + if data, ok := multiStreamData["data"]; ok { + rawData, err := json.Marshal(data) + if err != nil { + return err + } + switch streamType[1] { + case "trade": + var trade TradeStream + err := json.Unmarshal(rawData, &trade) + if err != nil { + return fmt.Errorf("%v - Could not unmarshal trade data: %s", + b.Name, + err) + } + + price, err := strconv.ParseFloat(trade.Price, 64) + if err != nil { + return fmt.Errorf("%v - price conversion error: %s", + b.Name, + err) + } + + amount, err := strconv.ParseFloat(trade.Quantity, 64) + if err != nil { + return fmt.Errorf("%v - amount conversion error: %s", + b.Name, + err) + } + + b.Websocket.DataHandler <- wshandler.TradeData{ + CurrencyPair: currency.NewPairFromFormattedPairs(trade.Symbol, b.GetEnabledPairs(asset.Spot), + b.GetPairFormat(asset.Spot, true)), + Timestamp: time.Unix(0, trade.TimeStamp*int64(time.Millisecond)), + Price: price, + Amount: amount, + Exchange: b.Name, + AssetType: asset.Spot, + } + case "ticker": + var t TickerStream + err := json.Unmarshal(rawData, &t) + if err != nil { + return fmt.Errorf("%v - Could not convert to a TickerStream structure %s", + b.Name, + err.Error()) + } + + b.Websocket.DataHandler <- &ticker.Price{ + ExchangeName: b.Name, + Open: t.OpenPrice, + Close: t.ClosePrice, + Volume: t.TotalTradedVolume, + QuoteVolume: t.TotalTradedQuoteVolume, + High: t.HighPrice, + Low: t.LowPrice, + Bid: t.BestBidPrice, + Ask: t.BestAskPrice, + Last: t.LastPrice, + LastUpdated: time.Unix(0, t.EventTime*int64(time.Millisecond)), + AssetType: asset.Spot, + Pair: currency.NewPairFromFormattedPairs(t.Symbol, b.GetEnabledPairs(asset.Spot), + b.GetPairFormat(asset.Spot, true)), + } + case "kline_1m", "kline_3m", "kline_5m", "kline_15m", "kline_30m", "kline_1h", "kline_2h", "kline_4h", + "kline_6h", "kline_8h", "kline_12h", "kline_1d", "kline_3d", "kline_1w", "kline_1M": + var kline KlineStream + err := json.Unmarshal(rawData, &kline) + if err != nil { + return fmt.Errorf("%v - Could not convert to a KlineStream structure %s", + b.Name, + err) + } + + b.Websocket.DataHandler <- wshandler.KlineData{ + Timestamp: time.Unix(0, kline.EventTime*int64(time.Millisecond)), + Pair: currency.NewPairFromFormattedPairs(kline.Symbol, b.GetEnabledPairs(asset.Spot), + b.GetPairFormat(asset.Spot, true)), + AssetType: asset.Spot, + Exchange: b.Name, + StartTime: time.Unix(0, kline.Kline.StartTime*int64(time.Millisecond)), + CloseTime: time.Unix(0, kline.Kline.CloseTime*int64(time.Millisecond)), + Interval: kline.Kline.Interval, + OpenPrice: kline.Kline.OpenPrice, + ClosePrice: kline.Kline.ClosePrice, + HighPrice: kline.Kline.HighPrice, + LowPrice: kline.Kline.LowPrice, + Volume: kline.Kline.Volume, + } + case "depth": + var depth WebsocketDepthStream + err := json.Unmarshal(rawData, &depth) + if err != nil { + return fmt.Errorf("%v - Could not convert to depthStream structure %s", + b.Name, + err) + } + + err = b.UpdateLocalCache(&depth) + if err != nil { + return fmt.Errorf("%v - UpdateLocalCache error: %s", + b.Name, + err) + } + + currencyPair := currency.NewPairFromFormattedPairs(depth.Pair, b.GetEnabledPairs(asset.Spot), + b.GetPairFormat(asset.Spot, true)) + b.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{ + Pair: currencyPair, + Asset: asset.Spot, + Exchange: b.Name, + } + default: + b.Websocket.DataHandler <- wshandler.UnhandledMessageWarning{Message: b.Name + wshandler.UnhandledMessage + string(respRaw)} + } + } + } + } + return nil +} + +func stringToOrderStatus(status string) (order.Status, error) { + switch status { + case "NEW": + return order.New, nil + case "CANCELLED": + return order.Cancelled, nil + case "REJECTED": + return order.Rejected, nil + case "TRADE": + return order.PartiallyFilled, nil + case "EXPIRED": + return order.Expired, nil + default: + return order.UnknownStatus, errors.New(status + " not recognised as order status") + } +} + // SeedLocalCache seeds depth data func (b *Binance) SeedLocalCache(p currency.Pair) error { var newOrderBook orderbook.Base @@ -294,7 +462,6 @@ func (b *Binance) UpdateLocalCache(wsdp *WebsocketDepthStream) error { } 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, diff --git a/exchanges/binance/binance_wrapper.go b/exchanges/binance/binance_wrapper.go index e17a9f27..443fcf7e 100644 --- a/exchanges/binance/binance_wrapper.go +++ b/exchanges/binance/binance_wrapper.go @@ -96,10 +96,16 @@ func (b *Binance) SetDefaults() { CryptoWithdrawalFee: true, }, WebsocketCapabilities: protocol.Features{ - TradeFetching: true, - TickerFetching: true, - KlineFetching: true, - OrderbookFetching: true, + TradeFetching: true, + TickerFetching: true, + KlineFetching: true, + OrderbookFetching: true, + AuthenticatedEndpoints: true, + AccountInfo: true, + GetOrder: true, + GetOrders: true, + Subscribe: true, + Unsubscribe: true, }, WithdrawPermissions: exchange.AutoWithdrawCrypto | exchange.NoFiatWithdrawals, @@ -415,14 +421,14 @@ func (b *Binance) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) { } var sideType string - if s.OrderSide == order.Buy { + if s.Side == order.Buy { sideType = order.Buy.String() } else { sideType = order.Sell.String() } var requestParamsOrderType RequestParamsOrderType - switch s.OrderType { + switch s.Type { case order.Market: requestParamsOrderType = BinanceRequestParamsOrderMarket case order.Limit: @@ -464,12 +470,12 @@ func (b *Binance) ModifyOrder(action *order.Modify) (string, error) { // CancelOrder cancels an order by its corresponding ID number func (b *Binance) CancelOrder(order *order.Cancel) error { - orderIDInt, err := strconv.ParseInt(order.OrderID, 10, 64) + orderIDInt, err := strconv.ParseInt(order.ID, 10, 64) if err != nil { return err } - _, err = b.CancelExistingOrder(b.FormatExchangeCurrency(order.CurrencyPair, + _, err = b.CancelExistingOrder(b.FormatExchangeCurrency(order.Pair, order.AssetType).String(), orderIDInt, order.AccountID) @@ -553,13 +559,13 @@ func (b *Binance) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) // GetActiveOrders retrieves any orders that are active/open func (b *Binance) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, error) { - if len(req.Currencies) == 0 { + if len(req.Pairs) == 0 { return nil, errors.New("at least one currency is required to fetch order history") } var orders []order.Detail - for x := range req.Currencies { - resp, err := b.OpenOrders(b.FormatExchangeCurrency(req.Currencies[x], + for x := range req.Pairs { + resp, err := b.OpenOrders(b.FormatExchangeCurrency(req.Pairs[x], asset.Spot).String()) if err != nil { return nil, err @@ -571,21 +577,21 @@ func (b *Binance) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, orderDate := time.Unix(0, int64(resp[i].Time)*int64(time.Millisecond)) orders = append(orders, order.Detail{ - Amount: resp[i].OrigQty, - OrderDate: orderDate, - Exchange: b.Name, - ID: strconv.FormatInt(resp[i].OrderID, 10), - OrderSide: orderSide, - OrderType: orderType, - Price: resp[i].Price, - Status: order.Status(resp[i].Status), - CurrencyPair: currency.NewPairFromString(resp[i].Symbol), + Amount: resp[i].OrigQty, + Date: orderDate, + Exchange: b.Name, + ID: strconv.FormatInt(resp[i].OrderID, 10), + Side: orderSide, + Type: orderType, + Price: resp[i].Price, + Status: order.Status(resp[i].Status), + Pair: currency.NewPairFromString(resp[i].Symbol), }) } } - order.FilterOrdersByType(&orders, req.OrderType) - order.FilterOrdersBySide(&orders, req.OrderSide) + order.FilterOrdersByType(&orders, req.Type) + order.FilterOrdersBySide(&orders, req.Side) order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) return orders, nil } @@ -593,13 +599,13 @@ func (b *Binance) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, // GetOrderHistory retrieves account order information // Can Limit response to specific order status func (b *Binance) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail, error) { - if len(req.Currencies) == 0 { + if len(req.Pairs) == 0 { return nil, errors.New("at least one currency is required to fetch order history") } var orders []order.Detail - for x := range req.Currencies { - resp, err := b.AllOrders(b.FormatExchangeCurrency(req.Currencies[x], + for x := range req.Pairs { + resp, err := b.AllOrders(b.FormatExchangeCurrency(req.Pairs[x], asset.Spot).String(), "", "1000") @@ -617,21 +623,21 @@ func (b *Binance) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail, } orders = append(orders, order.Detail{ - Amount: resp[i].OrigQty, - OrderDate: orderDate, - Exchange: b.Name, - ID: strconv.FormatInt(resp[i].OrderID, 10), - OrderSide: orderSide, - OrderType: orderType, - Price: resp[i].Price, - CurrencyPair: currency.NewPairFromString(resp[i].Symbol), - Status: order.Status(resp[i].Status), + Amount: resp[i].OrigQty, + Date: orderDate, + Exchange: b.Name, + ID: strconv.FormatInt(resp[i].OrderID, 10), + Side: orderSide, + Type: orderType, + Price: resp[i].Price, + Pair: currency.NewPairFromString(resp[i].Symbol), + Status: order.Status(resp[i].Status), }) } } - order.FilterOrdersByType(&orders, req.OrderType) - order.FilterOrdersBySide(&orders, req.OrderSide) + order.FilterOrdersByType(&orders, req.Type) + order.FilterOrdersBySide(&orders, req.Side) order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) return orders, nil } diff --git a/exchanges/bitfinex/bitfinex_test.go b/exchanges/bitfinex/bitfinex_test.go index 727739da..c7f34250 100644 --- a/exchanges/bitfinex/bitfinex_test.go +++ b/exchanges/bitfinex/bitfinex_test.go @@ -56,6 +56,9 @@ func TestMain(m *testing.M) { b.API.AuthenticatedSupport = true b.API.AuthenticatedWebsocketSupport = true } + b.WebsocketSubdChannels = make(map[int]WebsocketChanInfo) + b.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride() + b.Websocket.TrafficAlert = sharedtestvalues.GetWebsocketStructChannelOverride() os.Exit(m.Run()) } @@ -197,7 +200,6 @@ func TestNewDeposit(t *testing.T) { t.SkipNow() } t.Parallel() - b.Verbose = true _, err := b.NewDeposit("blabla", "testwallet", 0) if err == nil { t.Error("NewDeposit() Expected error") @@ -685,7 +687,7 @@ func TestFormatWithdrawPermissions(t *testing.T) { func TestGetActiveOrders(t *testing.T) { t.Parallel() var getOrdersRequest = order.GetOrdersRequest{ - OrderType: order.AnyType, + Type: order.AnyType, } _, err := b.GetActiveOrders(&getOrdersRequest) @@ -699,7 +701,7 @@ func TestGetActiveOrders(t *testing.T) { func TestGetOrderHistory(t *testing.T) { t.Parallel() var getOrdersRequest = order.GetOrdersRequest{ - OrderType: order.AnyType, + Type: order.AnyType, } _, err := b.GetOrderHistory(&getOrdersRequest) @@ -727,11 +729,11 @@ func TestSubmitOrder(t *testing.T) { Base: currency.BTC, Quote: currency.USD, }, - OrderSide: order.Buy, - OrderType: order.Limit, - Price: 1, - Amount: 1, - ClientID: "meowOrder", + Side: order.Buy, + Type: order.Limit, + Price: 1, + Amount: 1, + ClientID: "meowOrder", } response, err := b.SubmitOrder(orderSubmission) @@ -754,10 +756,10 @@ func TestCancelExchangeOrder(t *testing.T) { currencyPair := currency.NewPair(currency.LTC, currency.BTC) var orderCancellation = &order.Cancel{ - OrderID: "1", + ID: "1", WalletAddress: core.BitcoinDonationAddress, AccountID: "1", - CurrencyPair: currencyPair, + Pair: currencyPair, } err := b.CancelOrder(orderCancellation) @@ -777,10 +779,10 @@ func TestCancelAllExchangeOrdera(t *testing.T) { currencyPair := currency.NewPair(currency.LTC, currency.BTC) var orderCancellation = &order.Cancel{ - OrderID: "1", + ID: "1", WalletAddress: core.BitcoinDonationAddress, AccountID: "1", - CurrencyPair: currencyPair, + Pair: currencyPair, } resp, err := b.CancelAllOrders(orderCancellation) @@ -920,7 +922,7 @@ func setupWs() { } b.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride() b.Websocket.TrafficAlert = sharedtestvalues.GetWebsocketStructChannelOverride() - go b.WsReadData(b.AuthenticatedWebsocketConn) + go b.wsReadData(b.AuthenticatedWebsocketConn) go b.WsDataHandler() } @@ -1092,3 +1094,84 @@ func TestUpdateTradablePairs(t *testing.T) { t.Error(err) } } + +func TestWsSubscribedResponse(t *testing.T) { + pressXToJSON := `{"event":"subscribed","channel":"ticker","chanId":224555,"symbol":"tBTCUSD","pair":"BTCUSD"}` + err := b.wsHandleData([]byte(pressXToJSON)) + if err != nil { + t.Error(err) + } +} + +func TestWsTradingPairSnapshot(t *testing.T) { + b.WebsocketSubdChannels[23405] = WebsocketChanInfo{Pair: "BTCUSD", Channel: wsBook} + pressXToJSON := `[23405,[[38334303613,9348.8,0.53],[38334308111,9348.8,5.98979404],[38331335157,9344.1,1.28965787],[38334302803,9343.8,0.08230094],[38334279092,9343,0.8],[38334307036,9342.938663676,0.8],[38332749107,9342.9,0.2],[38332277330,9342.8,0.85],[38329406786,9342,0.1432012],[38332841570,9341.947288638,0.3],[38332163238,9341.7,0.3],[38334303384,9341.6,0.324],[38332464840,9341.4,0.5],[38331935870,9341.2,0.5],[38334312082,9340.9,0.02126899],[38334261292,9340.8,0.26763],[38334138680,9340.625455254,0.12],[38333896802,9339.8,0.85],[38331627527,9338.9,1.57863959],[38334186713,9338.9,0.26769],[38334305819,9338.8,2.999],[38334211180,9338.75285796,3.999],[38334310699,9337.8,0.10679883],[38334307414,9337.5,1],[38334179822,9337.1,0.26773],[38334306600,9336.659955102,1.79],[38334299667,9336.6,1.1],[38334306452,9336.6,0.13979771],[38325672859,9336.3,1.25],[38334311646,9336.2,1],[38334258509,9336.1,0.37],[38334310592,9336,1.79],[38334310378,9335.6,1.43],[38334132444,9335.2,0.26777],[38331367325,9335,0.07],[38334310703,9335,0.10680562],[38334298209,9334.7,0.08757301],[38334304857,9334.456899462,0.291],[38334309940,9334.088390727,0.0725],[38334310377,9333.7,1.2868],[38334297615,9333.607784,0.1108],[38334095188,9333.3,0.26785],[38334228913,9332.7,0.40861186],[38334300526,9332.363996604,0.3884],[38334310701,9332.2,0.10680562],[38334303548,9332.005382871,0.07],[38334311798,9331.8,0.41285228],[38334301012,9331.7,1.7952],[38334089877,9331.4,0.2679],[38321942150,9331.2,0.2],[38334310670,9330,1.069],[38334063096,9329.6,0.26796],[38334310700,9329.4,0.10680562],[38334310404,9329.3,1],[38334281630,9329.1,6.57150597],[38334036864,9327.7,0.26801],[38334310702,9326.6,0.10680562],[38334311799,9326.1,0.50220625],[38334164163,9326,0.219638],[38334309722,9326,1.5],[38333051682,9325.8,0.26807],[38334302027,9325.7,0.75],[38334203435,9325.366592,0.32397696],[38321967613,9325,0.05],[38334298787,9324.9,0.3],[38334301719,9324.8,3.6227592],[38331316716,9324.763454646,0.71442],[38334310698,9323.8,0.10680562],[38334035499,9323.7,0.23431017],[38334223472,9322.670551788,0.42150603],[38334163459,9322.560399006,0.143967],[38321825171,9320.8,2],[38334075805,9320.467496148,0.30772633],[38334075800,9319.916732238,0.61457592],[38333682302,9319.7,0.0011],[38331323088,9319.116771762,0.12913],[38333677480,9319,0.0199],[38334277797,9318.6,0.89],[38325235155,9318.041088,1.20249],[38334310910,9317.82382938,1.79],[38334311811,9317.2,0.61079138],[38334311812,9317.2,0.71937652],[38333298214,9317.1,50],[38334306359,9317,1.79],[38325531545,9316.382823951,0.21263],[38333727253,9316.3,0.02316372],[38333298213,9316.1,45],[38333836479,9316,2.135],[38324520465,9315.9,2.7681],[38334307411,9315.5,1],[38330313617,9315.3,0.84455],[38334077770,9315.294024,0.01248397],[38334286663,9315.294024,1],[38325533762,9315.290315394,2.40498],[38334310018,9315.2,3],[38333682617,9314.6,0.0011],[38334304794,9314.6,0.76364676],[38334304798,9314.3,0.69242113],[38332915733,9313.8,0.0199],[38334084411,9312.8,1],[38334311893,9350.1,-1.015],[38334302734,9350.3,-0.26737],[38334300732,9350.8,-5.2],[38333957619,9351,-0.90677089],[38334300521,9351,-1.6457],[38334301600,9351.012829557,-0.0523],[38334308878,9351.7,-2.5],[38334299570,9351.921544,-0.1015],[38334279367,9352.1,-0.26732],[38334299569,9352.411802928,-0.4036],[38334202773,9353.4,-0.02139404],[38333918472,9353.7,-1.96412776],[38334278782,9354,-0.26731],[38334278606,9355,-1.2785],[38334302105,9355.439221251,-0.79191542],[38313897370,9355.569409242,-0.43363],[38334292995,9355.584296,-0.0979],[38334216989,9355.8,-0.03686414],[38333894025,9355.9,-0.26721],[38334293798,9355.936691952,-0.4311],[38331159479,9356,-0.4204022],[38333918888,9356.1,-1.10885563],[38334298205,9356.4,-0.20124428],[38328427481,9356.5,-0.1],[38333343289,9356.6,-0.41034213],[38334297205,9356.6,-0.08835018],[38334277927,9356.741101161,-0.0737],[38334311645,9356.8,-0.5],[38334309002,9356.9,-5],[38334309736,9357,-0.10680107],[38334306448,9357.4,-0.18645275],[38333693302,9357.7,-0.2672],[38332815159,9357.8,-0.0011],[38331239824,9358.2,-0.02],[38334271608,9358.3,-2.999],[38334311971,9358.4,-0.55],[38333919260,9358.5,-1.9972841],[38334265365,9358.5,-1.7841],[38334277960,9359,-3],[38334274601,9359.020969848,-3],[38326848839,9359.1,-0.84],[38334291080,9359.247048,-0.16199869],[38326848844,9359.4,-1.84],[38333680200,9359.6,-0.26713],[38331326606,9359.8,-0.84454],[38334309738,9359.8,-0.10680107],[38331314707,9359.9,-0.2],[38333919803,9360.9,-1.41177599],[38323651149,9361.33417827,-0.71442],[38333656906,9361.5,-0.26705],[38334035500,9361.5,-0.40861586],[38334091886,9362.4,-6.85940815],[38334269617,9362.5,-4],[38323629409,9362.545858872,-2.40497],[38334309737,9362.7,-0.10680107],[38334312380,9362.7,-3],[38325280830,9362.8,-1.75123],[38326622800,9362.8,-1.05145],[38333175230,9363,-0.0011],[38326848745,9363.2,-0.79],[38334308960,9363.206775564,-0.12],[38333920234,9363.3,-1.25318113],[38326848843,9363.4,-1.29],[38331239823,9363.4,-0.02],[38333209613,9363.4,-0.26719],[38334299964,9364,-0.05583123],[38323470224,9364.161816648,-0.12912],[38334284711,9365,-0.21346019],[38334299594,9365,-2.6757062],[38323211816,9365.073132585,-0.21262],[38334312456,9365.1,-0.11167861],[38333209612,9365.2,-0.26719],[38327770474,9365.3,-0.0073],[38334298788,9365.3,-0.3],[38334075803,9365.409831204,-0.30772637],[38334309740,9365.5,-0.10680107],[38326608767,9365.7,-2.76809],[38333920657,9365.7,-1.25848083],[38329594226,9366.6,-0.02587],[38334311813,9366.7,-4.72290945],[38316386301,9367.39258128,-2.37581],[38334302026,9367.4,-4.5],[38334228915,9367.9,-0.81725458],[38333921381,9368.1,-1.72213641],[38333175678,9368.2,-0.0011],[38334301150,9368.2,-2.654604],[38334297208,9368.3,-0.78036466],[38334309739,9368.3,-0.10680107],[38331227515,9368.7,-0.02],[38331184470,9369,-0.003975],[38334203436,9369.319616,-0.32397695],[38334269964,9369.7,-0.5],[38328386732,9370,-4.11759935],[38332719555,9370,-0.025],[38333921935,9370.5,-1.2224398],[38334258511,9370.5,-0.35],[38326848842,9370.8,-0.34],[38333985038,9370.9,-0.8551502],[38334283018,9370.9,-1],[38326848744,9371,-1.34]]]` + err := b.wsHandleData([]byte(pressXToJSON)) + if err != nil { + t.Error(err) + } + pressXToJSON = `[23405,[7617,52.98726298,7617.1,53.601795929999994,-550.9,-0.0674,7617,8318.92961981,8257.8,7500]]` + err = b.wsHandleData([]byte(pressXToJSON)) + if err != nil { + t.Error(err) + } +} + +func TestWsTradeResponse(t *testing.T) { + b.WebsocketSubdChannels[18788] = WebsocketChanInfo{Pair: "BTCUSD", Channel: wsTrades} + pressXToJSON := `[18788,[[412685577,1580268444802,11.1998,176.3],[412685575,1580268444802,5,176.29952759],[412685574,1580268374717,1.99069999,176.41],[412685573,1580268374717,1.00930001,176.41],[412685572,1580268358760,0.9907,176.47],[412685571,1580268324362,0.5505,176.44],[412685570,1580268297270,-0.39040819,176.39],[412685568,1580268297270,-0.39780162,176.46475676],[412685567,1580268283470,-0.09,176.41],[412685566,1580268256536,-2.31310783,176.48],[412685565,1580268256536,-0.59669217,176.49],[412685564,1580268256536,-0.9902,176.49],[412685562,1580268194474,0.9902,176.55],[412685561,1580268186215,0.1,176.6],[412685560,1580268185964,-2.17096773,176.5],[412685559,1580268185964,-1.82903227,176.51],[412685558,1580268181215,2.098914,176.53],[412685557,1580268169844,16.7302,176.55],[412685556,1580268169844,3.25,176.54],[412685555,1580268155725,0.23576115,176.45],[412685553,1580268155725,3,176.44596249],[412685552,1580268155725,3.25,176.44],[412685551,1580268155725,5,176.44],[412685550,1580268155725,0.65830078,176.41],[412685549,1580268155725,0.45063807,176.41],[412685548,1580268153825,-0.67604704,176.39],[412685547,1580268145713,2.5883,176.41],[412685543,1580268087513,12.92927,176.33],[412685542,1580268087513,0.40083,176.33],[412685533,1580268005756,-0.17096773,176.32]]]` + err := b.wsHandleData([]byte(pressXToJSON)) + if err != nil { + t.Error(err) + } +} + +func TestWsTickerResponse(t *testing.T) { + b.WebsocketSubdChannels[11534] = WebsocketChanInfo{Pair: "BTCUSD", Channel: wsTicker} + pressXToJSON := `[11534,[61.304,2228.36155358,61.305,1323.2442970500003,0.395,0.0065,61.371,50973.3020771,62.5,57.421]]` + err := b.wsHandleData([]byte(pressXToJSON)) + if err != nil { + t.Error(err) + } +} + +func TestWsCandleResponse(t *testing.T) { + b.WebsocketSubdChannels[343351] = WebsocketChanInfo{Pair: "BTCUSD", Channel: wsCandles} + pressXToJSON := `[343351,[[1574698260000,7379.785503,7383.8,7388.3,7379.785503,1.68829482]]]` + err := b.wsHandleData([]byte(pressXToJSON)) + if err != nil { + t.Error(err) + } + pressXToJSON = `[343351,[1574698200000,7399.9,7379.7,7399.9,7371.8,41.63633658]]` + err = b.wsHandleData([]byte(pressXToJSON)) + if err != nil { + t.Error(err) + } +} + +func TestWsOrderSnapshot(t *testing.T) { + pressXToJSON := `[0,"os",[[34930659963,null,1574955083558,"tETHUSD",1574955083558,1574955083573,0.201104,0.201104,"EXCHANGE LIMIT",null,null,null,0,"ACTIVE",null,null,120,0,0,0,null,null,null,0,0,null,null,null,"BFX",null,null,null]]]` + err := b.wsHandleData([]byte(pressXToJSON)) + if err != nil { + t.Error(err) + } + pressXToJSON = `[0,"oc",[34930659963,null,1574955083558,"tETHUSD",1574955083558,1574955354487,0.201104,0.201104,"EXCHANGE LIMIT",null,null,null,0,"CANCELED",null,null,120,0,0,0,null,null,null,0,0,null,null,null,"BFX",null,null,null]]` + err = b.wsHandleData([]byte(pressXToJSON)) + if err != nil { + t.Error(err) + } +} + +func TestWsNotifications(t *testing.T) { + pressXToJSON := `[0,"n",[1575282446099,"fon-req",null,null,[41238905,null,null,null,-1000,null,null,null,null,null,null,null,null,null,0.002,2,null,null,null,null,null],null,"SUCCESS","Submitting funding bid of 1000.0 USD at 0.2000 for 2 days."]]` + err := b.wsHandleData([]byte(pressXToJSON)) + if err != nil { + t.Error(err) + } + + pressXToJSON = `[0,"n",[1575287438.515,"on-req",null,null,[1185815098,null,1575287436979,"tETHUSD",1575287438515,1575287438515,-2.5,-2.5,"LIMIT",null,null,null,0,"ACTIVE",null,null,230,0,0,0,null,null,null,0,null,null,null,null,"API>BFX",null,null,null],null,"SUCCESS","Submitting limit sell order for -2.5 ETH."]]` + err = b.wsHandleData([]byte(pressXToJSON)) + if err != nil { + t.Error(err) + } +} diff --git a/exchanges/bitfinex/bitfinex_types.go b/exchanges/bitfinex/bitfinex_types.go index 7cec1fac..3d377c0c 100644 --- a/exchanges/bitfinex/bitfinex_types.go +++ b/exchanges/bitfinex/bitfinex_types.go @@ -438,7 +438,7 @@ type WebsocketOrder struct { Status string Price float64 PriceAvg float64 - Timestamp string + Timestamp int64 Notify int } @@ -507,13 +507,21 @@ const ( wsBalanceUpdate = "bu" wsMarginInfoUpdate = "miu" wsNotification = "n" + wsOrderSnapshot = "os" wsOrderNew = "on" wsOrderUpdate = "ou" wsOrderCancel = "oc" + wsRequest = "-req" + wsOrderNewRequest = wsOrderNew + wsRequest + wsOrderUpdateRequest = wsOrderUpdate + wsRequest + wsOrderCancelRequest = wsOrderCancel + wsRequest wsFundingOrderSnapshot = "fos" wsFundingOrderNew = "fon" wsFundingOrderUpdate = "fou" wsFundingOrderCancel = "foc" + wsFundingOrderNewRequest = wsFundingOrderNew + wsRequest + wsFundingOrderUpdateRequest = wsFundingOrderUpdate + wsRequest + wsFundingOrderCancelRequest = wsFundingOrderCancel + wsRequest wsCancelMultipleOrders = "oc_multi" wsBook = "book" wsCandles = "candles" @@ -534,22 +542,22 @@ type WsAuthRequest struct { // WsFundingOffer funding offer received via websocket type WsFundingOffer struct { - ID int64 - Symbol string - Created int64 - Updated int64 - Amount float64 - AmountOrig float64 - Type string - Flags interface{} - Status string - Rate float64 - Period int64 - Notify bool - Hidden bool - Insure bool - Renew bool - RateReal float64 + ID int64 + Symbol string + Created int64 + Updated int64 + Amount float64 + OriginalAmount float64 + Type string + Flags interface{} + Status string + Rate float64 + Period int64 + Notify bool + Hidden bool + Insure bool + Renew bool + RateReal float64 } // WsCredit credit details received via websocket diff --git a/exchanges/bitfinex/bitfinex_websocket.go b/exchanges/bitfinex/bitfinex_websocket.go index 08036449..13dd78c0 100644 --- a/exchanges/bitfinex/bitfinex_websocket.go +++ b/exchanges/bitfinex/bitfinex_websocket.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "net/http" - "reflect" "strconv" "strings" "time" @@ -36,7 +35,7 @@ func (b *Bitfinex) WsConnect() error { if err != nil { return fmt.Errorf("%v unable to connect to Websocket. Error: %s", b.Name, err) } - go b.WsReadData(b.WebsocketConn) + go b.wsReadData(b.WebsocketConn) if b.Websocket.CanUseAuthenticatedEndpoints() { err = b.AuthenticatedWebsocketConn.Dial(&dialer, http.Header{}) @@ -44,7 +43,7 @@ func (b *Bitfinex) WsConnect() error { log.Errorf(log.ExchangeSys, "%v unable to connect to authenticated Websocket. Error: %s", b.Name, err) b.Websocket.SetCanUseAuthenticatedEndpoints(false) } - go b.WsReadData(b.AuthenticatedWebsocketConn) + go b.wsReadData(b.AuthenticatedWebsocketConn) err = b.WsSendAuth() if err != nil { log.Errorf(log.ExchangeSys, "%v - authentication failed: %v\n", b.Name, err) @@ -57,8 +56,8 @@ func (b *Bitfinex) WsConnect() error { return nil } -// WsReadData funnels both auth and public ws data into one manageable place -func (b *Bitfinex) WsReadData(ws *wshandler.WebsocketConnection) { +// wsReadData receives and passes on websocket messages for processing +func (b *Bitfinex) wsReadData(ws *wshandler.WebsocketConnection) { b.Websocket.Wg.Add(1) defer b.Websocket.Wg.Done() for { @@ -68,7 +67,7 @@ func (b *Bitfinex) WsReadData(ws *wshandler.WebsocketConnection) { default: resp, err := ws.ReadMessage() if err != nil { - b.Websocket.DataHandler <- err + b.Websocket.ReadMessageErrors <- err return } b.Websocket.TrafficAlert <- struct{}{} @@ -77,563 +76,683 @@ func (b *Bitfinex) WsReadData(ws *wshandler.WebsocketConnection) { } } -// WsDataHandler handles data from WsReadData +// WsDataHandler handles data from wsReadData func (b *Bitfinex) WsDataHandler() { b.Websocket.Wg.Add(1) defer b.Websocket.Wg.Done() - for { select { case <-b.Websocket.ShutdownC: return - case stream := <-comms: - if stream.Type == websocket.TextMessage { - var result interface{} - err := json.Unmarshal(stream.Raw, &result) + case resp := <-comms: + if resp.Type == websocket.TextMessage { + err := b.wsHandleData(resp.Raw) if err != nil { b.Websocket.DataHandler <- err - return - } - switch reflect.TypeOf(result).String() { - case "map[string]interface {}": - eventData := result.(map[string]interface{}) - event := eventData["event"] - switch event { - case "subscribed": - if symbol, ok := eventData["pair"].(string); ok { - b.WsAddSubscriptionChannel(int(eventData["chanId"].(float64)), - eventData["channel"].(string), - symbol, - ) - } else if key, ok := eventData["key"].(string); ok { - b.WsAddSubscriptionChannel(int(eventData["chanId"].(float64)), - eventData["channel"].(string), - key, - ) - } - case "auth": - status := eventData["status"].(string) - if status == "OK" { - b.Websocket.DataHandler <- eventData - b.WsAddSubscriptionChannel(0, "account", "N/A") - } else if status == "fail" { - b.Websocket.DataHandler <- fmt.Errorf("bitfinex.go error - Websocket unable to AUTH. Error code: %s", - eventData["code"].(string)) - } - } - case "[]interface {}": - chanData := result.([]interface{}) - if hb, ok := chanData[1].(string); ok { - // Capturing heart beat - if hb == "hb" { - continue - } - } - chanID := int(chanData[0].(float64)) - chanInfo, ok := b.WebsocketSubdChannels[chanID] - if !ok && chanID != 0 { - b.Websocket.DataHandler <- fmt.Errorf("bitfinex.go error - Unable to locate chanID: %d", - chanID) - continue - } - - switch chanInfo.Channel { - case wsBook: - var newOrderbook []WebsocketBook - curr := currency.NewPairFromString(chanInfo.Pair) - if obSnapBundle, ok := chanData[1].([]interface{}); ok { - switch id := obSnapBundle[0].(type) { - case []interface{}: - for i := range obSnapBundle { - data := obSnapBundle[i].([]interface{}) - newOrderbook = append(newOrderbook, WebsocketBook{ - ID: int64(data[0].(float64)), - Price: data[1].(float64), - Amount: data[2].(float64)}) - } - err := b.WsInsertSnapshot(curr, - asset.Spot, - newOrderbook) - if err != nil { - b.Websocket.DataHandler <- fmt.Errorf("bitfinex_websocket.go inserting snapshot error: %s", - err) - } - case float64: - newOrderbook = append(newOrderbook, WebsocketBook{ - ID: int64(id), - Price: obSnapBundle[1].(float64), - Amount: obSnapBundle[2].(float64)}) - err := b.WsUpdateOrderbook(curr, - asset.Spot, - newOrderbook) - if err != nil { - b.Websocket.DataHandler <- fmt.Errorf("bitfinex_websocket.go inserting snapshot error: %s", - err) - } - } - } - continue - case wsCandles: - curr := currency.NewPairFromString(chanInfo.Pair) - if candleBundle, ok := chanData[1].([]interface{}); ok { - if len(candleBundle) == 0 { - continue - } - switch candleBundle[0].(type) { - case []interface{}: - for i := range candleBundle { - candle := candleBundle[i].([]interface{}) - b.Websocket.DataHandler <- wshandler.KlineData{ - Timestamp: time.Unix(0, candle[0].(int64)), - Exchange: b.Name, - AssetType: asset.Spot, - Pair: curr, - OpenPrice: candle[1].(float64), - ClosePrice: candle[2].(float64), - HighPrice: candle[3].(float64), - LowPrice: candle[4].(float64), - Volume: candle[5].(float64), - } - } - case float64: - b.Websocket.DataHandler <- wshandler.KlineData{ - Timestamp: time.Unix(0, candleBundle[0].(int64)), - Exchange: b.Name, - AssetType: asset.Spot, - Pair: curr, - OpenPrice: candleBundle[1].(float64), - ClosePrice: candleBundle[2].(float64), - HighPrice: candleBundle[3].(float64), - LowPrice: candleBundle[4].(float64), - Volume: candleBundle[5].(float64), - } - } - } - continue - case wsTicker: - tickerData := chanData[1].([]interface{}) - b.Websocket.DataHandler <- &ticker.Price{ - ExchangeName: b.Name, - Bid: tickerData[0].(float64), - Ask: tickerData[2].(float64), - Last: tickerData[6].(float64), - Volume: tickerData[7].(float64), - High: tickerData[8].(float64), - Low: tickerData[9].(float64), - AssetType: asset.Spot, - Pair: currency.NewPairFromString(chanInfo.Pair), - } - continue - case wsTrades: - var trades []WebsocketTrade - switch len(chanData) { - case 2: - snapshot := chanData[1].([]interface{}) - for i := range snapshot { - elem := snapshot[i].([]interface{}) - if len(elem) == 5 { - trades = append(trades, - WebsocketTrade{ - ID: int64(elem[0].(float64)), - Timestamp: int64(elem[1].(float64)), - Amount: elem[3].(float64), - Rate: elem[4].(float64), - Period: int64(elem[4].(float64)), - }) - continue - } - trades = append(trades, - WebsocketTrade{ - ID: int64(elem[0].(float64)), - Timestamp: int64(elem[1].(float64)), - Price: elem[3].(float64), - Amount: elem[2].(float64), - }) - } - case 3: - if chanData[1].(string) == wsTradeExecutionUpdate || - chanData[1].(string) == wsFundingTradeUpdate { - // "(f)te - trade executed" && "(f)tu - trade updated" - // contain the same amount of data - // "(f)te" gets sent first so we can drop "(f)tu" - continue - } - data := chanData[2].([]interface{}) - trades = append(trades, WebsocketTrade{ - ID: int64(data[0].(float64)), - Timestamp: int64(data[1].(float64)), - Price: data[3].(float64), - Amount: data[2].(float64)}) - } - - for i := range trades { - side := order.Buy.String() - newAmount := trades[i].Amount - if newAmount < 0 { - side = order.Sell.String() - newAmount *= -1 - } - - if trades[i].Rate > 0 { - b.Websocket.DataHandler <- wshandler.FundingData{ - CurrencyPair: currency.NewPairFromString(chanInfo.Pair), - Timestamp: time.Unix(0, trades[i].Timestamp*int64(time.Millisecond)), - Amount: newAmount, - Exchange: b.Name, - AssetType: asset.Spot, - Side: side, - Rate: trades[i].Rate, - Period: trades[i].Period, - } - continue - } - - b.Websocket.DataHandler <- wshandler.TradeData{ - CurrencyPair: currency.NewPairFromString(chanInfo.Pair), - Timestamp: time.Unix(0, trades[i].Timestamp*int64(time.Millisecond)), - Price: trades[i].Price, - Amount: newAmount, - Exchange: b.Name, - AssetType: asset.Spot, - Side: side, - } - } - continue - } - - if authResp, ok := chanData[1].(string); ok { - switch authResp { - case wsHeartbeat, pong: - continue - case wsNotification: - notification := chanData[2].([]interface{}) - if data, ok := notification[4].([]interface{}); ok { - channelName := notification[1].(string) - switch { - case strings.Contains(channelName, wsOrderUpdate), - strings.Contains(channelName, wsOrderCancel), - strings.Contains(channelName, wsFundingOrderCancel): - if data[0] != nil && data[0].(float64) > 0 { - b.AuthenticatedWebsocketConn.AddResponseWithID(int64(data[0].(float64)), stream.Raw) - continue - } - case strings.Contains(channelName, wsOrderNew): - if data[2] != nil && data[2].(float64) > 0 { - b.AuthenticatedWebsocketConn.AddResponseWithID(int64(data[2].(float64)), stream.Raw) - continue - } - } - b.Websocket.DataHandler <- fmt.Errorf("%s - Unexpected data returned %s", b.Name, stream.Raw) - continue - } - if notification[5] != nil && strings.EqualFold(notification[5].(string), wsError) { - b.Websocket.DataHandler <- fmt.Errorf("%s - Error %s", b.Name, notification[6].(string)) - } - case wsPositionSnapshot: - var snapshot []WebsocketPosition - if snapBundle, ok := chanData[2].([]interface{}); ok && len(snapBundle) > 0 { - if _, ok := snapBundle[0].([]interface{}); ok { - for i := range snapBundle { - positionData := snapBundle[i].([]interface{}) - position := WebsocketPosition{ - Pair: positionData[0].(string), - Status: positionData[1].(string), - Amount: positionData[2].(float64), - Price: positionData[3].(float64), - MarginFunding: positionData[4].(float64), - MarginFundingType: int64(positionData[5].(float64)), - ProfitLoss: positionData[6].(float64), - ProfitLossPercent: positionData[7].(float64), - LiquidationPrice: positionData[8].(float64), - Leverage: positionData[9].(float64), - } - snapshot = append(snapshot, position) - } - b.Websocket.DataHandler <- snapshot - } - } - case wsPositionNew, wsPositionUpdate, wsPositionClose: - if positionData, ok := chanData[2].([]interface{}); ok && len(positionData) > 0 { - position := WebsocketPosition{ - Pair: positionData[0].(string), - Status: positionData[1].(string), - Amount: positionData[2].(float64), - Price: positionData[3].(float64), - MarginFunding: positionData[4].(float64), - MarginFundingType: int64(positionData[5].(float64)), - ProfitLoss: positionData[6].(float64), - ProfitLossPercent: positionData[7].(float64), - LiquidationPrice: positionData[8].(float64), - Leverage: positionData[9].(float64), - } - b.Websocket.DataHandler <- position - } - case wsTradeExecutionUpdate: - if tradeData, ok := chanData[2].([]interface{}); ok && len(tradeData) > 4 { - b.Websocket.DataHandler <- WebsocketTradeData{ - TradeID: int64(tradeData[0].(float64)), - Pair: tradeData[1].(string), - Timestamp: int64(tradeData[2].(float64)), - OrderID: int64(tradeData[3].(float64)), - AmountExecuted: tradeData[4].(float64), - PriceExecuted: tradeData[5].(float64), - OrderType: tradeData[6].(string), - OrderPrice: tradeData[7].(float64), - Maker: tradeData[8].(float64) == 1, - Fee: tradeData[9].(float64), - FeeCurrency: tradeData[10].(string), - } - } - case wsFundingOrderSnapshot: - var snapshot []WsFundingOffer - if snapBundle, ok := chanData[2].([]interface{}); ok && len(snapBundle) > 0 { - if _, ok := snapBundle[0].([]interface{}); ok { - for i := range snapBundle { - data := snapBundle[i].([]interface{}) - offer := WsFundingOffer{ - ID: int64(data[0].(float64)), - Symbol: data[1].(string), - Created: int64(data[2].(float64)), - Updated: int64(data[3].(float64)), - Amount: data[4].(float64), - AmountOrig: data[5].(float64), - Type: data[6].(string), - Flags: data[9].(float64), - Status: data[10].(string), - Rate: data[14].(float64), - Period: int64(data[15].(float64)), - Notify: data[16].(float64) == 1, - Hidden: data[17].(float64) == 1, - Insure: data[18].(float64) == 1, - Renew: data[19].(float64) == 1, - RateReal: data[20].(float64), - } - snapshot = append(snapshot, offer) - } - b.Websocket.DataHandler <- snapshot - } - } - case wsFundingOrderNew, wsFundingOrderUpdate, wsFundingOrderCancel: - if data, ok := chanData[2].([]interface{}); ok && len(data) > 0 { - b.Websocket.DataHandler <- WsFundingOffer{ - ID: int64(data[0].(float64)), - Symbol: data[1].(string), - Created: int64(data[2].(float64)), - Updated: int64(data[3].(float64)), - Amount: data[4].(float64), - AmountOrig: data[5].(float64), - Type: data[6].(string), - Flags: data[9].(float64), - Status: data[10].(string), - Rate: data[14].(float64), - Period: int64(data[15].(float64)), - Notify: data[16].(float64) == 1, - Hidden: data[17].(float64) == 1, - Insure: data[18].(float64) == 1, - Renew: data[19].(float64) == 1, - RateReal: data[20].(float64), - } - } - case wsFundingCreditSnapshot: - var snapshot []WsCredit - if snapBundle, ok := chanData[2].([]interface{}); ok && len(snapBundle) > 0 { - if _, ok := snapBundle[0].([]interface{}); ok { - for i := range snapBundle { - data := snapBundle[i].([]interface{}) - credit := WsCredit{ - ID: int64(data[0].(float64)), - Symbol: data[1].(string), - Side: data[2].(string), - Created: int64(data[3].(float64)), - Updated: int64(data[4].(float64)), - Amount: data[5].(float64), - Flags: data[6].(string), - Status: data[7].(string), - Rate: data[11].(float64), - Period: int64(data[12].(float64)), - Opened: int64(data[13].(float64)), - LastPayout: int64(data[14].(float64)), - Notify: data[15].(float64) == 1, - Hidden: data[16].(float64) == 1, - Insure: data[17].(float64) == 1, - Renew: data[18].(float64) == 1, - RateReal: data[19].(float64), - NoClose: data[20].(float64) == 1, - PositionPair: data[21].(string), - } - snapshot = append(snapshot, credit) - } - b.Websocket.DataHandler <- snapshot - } - } - case wsFundingCreditNew, wsFundingCreditUpdate, wsFundingCreditCancel: - if data, ok := chanData[2].([]interface{}); ok && len(data) > 0 { - b.Websocket.DataHandler <- WsCredit{ - ID: int64(data[0].(float64)), - Symbol: data[1].(string), - Side: data[2].(string), - Created: int64(data[3].(float64)), - Updated: int64(data[4].(float64)), - Amount: data[5].(float64), - Flags: data[6].(string), - Status: data[7].(string), - Rate: data[11].(float64), - Period: int64(data[12].(float64)), - Opened: int64(data[13].(float64)), - LastPayout: int64(data[14].(float64)), - Notify: data[15].(float64) == 1, - Hidden: data[16].(float64) == 1, - Insure: data[17].(float64) == 1, - Renew: data[18].(float64) == 1, - RateReal: data[19].(float64), - NoClose: data[20].(float64) == 1, - PositionPair: data[21].(string), - } - } - case wsFundingLoanSnapshot: - var snapshot []WsCredit - if snapBundle, ok := chanData[2].([]interface{}); ok && len(snapBundle) > 0 { - if _, ok := snapBundle[0].([]interface{}); ok { - for i := range snapBundle { - data := snapBundle[i].([]interface{}) - credit := WsCredit{ - ID: int64(data[0].(float64)), - Symbol: data[1].(string), - Side: data[2].(string), - Created: int64(data[3].(float64)), - Updated: int64(data[4].(float64)), - Amount: data[5].(float64), - Flags: data[6].(string), - Status: data[7].(string), - Rate: data[11].(float64), - Period: int64(data[12].(float64)), - Opened: int64(data[13].(float64)), - LastPayout: int64(data[14].(float64)), - Notify: data[15].(float64) == 1, - Hidden: data[16].(float64) == 1, - Insure: data[17].(float64) == 1, - Renew: data[18].(float64) == 1, - RateReal: data[19].(float64), - NoClose: data[20].(float64) == 1, - } - snapshot = append(snapshot, credit) - } - b.Websocket.DataHandler <- snapshot - } - } - case wsFundingLoanNew, wsFundingLoanUpdate, wsFundingLoanCancel: - if data, ok := chanData[2].([]interface{}); ok && len(data) > 0 { - b.Websocket.DataHandler <- WsCredit{ - ID: int64(data[0].(float64)), - Symbol: data[1].(string), - Side: data[2].(string), - Created: int64(data[3].(float64)), - Updated: int64(data[4].(float64)), - Amount: data[5].(float64), - Flags: data[6].(string), - Status: data[7].(string), - Rate: data[11].(float64), - Period: int64(data[12].(float64)), - Opened: int64(data[13].(float64)), - LastPayout: int64(data[14].(float64)), - Notify: data[15].(float64) == 1, - Hidden: data[16].(float64) == 1, - Insure: data[17].(float64) == 1, - Renew: data[18].(float64) == 1, - RateReal: data[19].(float64), - NoClose: data[20].(float64) == 1, - } - } - case wsWalletSnapshot: - var snapshot []WsWallet - if snapBundle, ok := chanData[2].([]interface{}); ok && len(snapBundle) > 0 { - if _, ok := snapBundle[0].([]interface{}); ok { - for i := range snapBundle { - data := snapBundle[i].([]interface{}) - var balanceAvailable float64 - if _, ok := data[4].(float64); ok { - balanceAvailable = data[4].(float64) - } - wallet := WsWallet{ - Type: data[0].(string), - Currency: data[1].(string), - Balance: data[2].(float64), - UnsettledInterest: data[3].(float64), - BalanceAvailable: balanceAvailable, - } - snapshot = append(snapshot, wallet) - } - b.Websocket.DataHandler <- snapshot - } - } - case wsWalletUpdate: - if data, ok := chanData[2].([]interface{}); ok && len(data) > 0 { - var balanceAvailable float64 - if _, ok := data[4].(float64); ok { - balanceAvailable = data[4].(float64) - } - b.Websocket.DataHandler <- WsWallet{ - Type: data[0].(string), - Currency: data[1].(string), - Balance: data[2].(float64), - UnsettledInterest: data[3].(float64), - BalanceAvailable: balanceAvailable, - } - } - case wsBalanceUpdate: - if data, ok := chanData[2].([]interface{}); ok && len(data) > 0 { - b.Websocket.DataHandler <- WsBalanceInfo{ - TotalAssetsUnderManagement: data[0].(float64), - NetAssetsUnderManagement: data[1].(float64), - } - } - case wsMarginInfoUpdate: - if data, ok := chanData[2].([]interface{}); ok && len(data) > 0 { - if data[0].(string) == "base" { - if infoBase, ok := chanData[2].([]interface{}); ok && len(infoBase) > 0 { - baseData := data[1].([]interface{}) - b.Websocket.DataHandler <- WsMarginInfoBase{ - UserProfitLoss: baseData[0].(float64), - UserSwaps: baseData[1].(float64), - MarginBalance: baseData[2].(float64), - MarginNet: baseData[3].(float64), - } - } - } - } - case wsFundingInfoUpdate: - if data, ok := chanData[2].([]interface{}); ok && len(data) > 0 { - if data[0].(string) == "sym" { - symbolData := data[1].([]interface{}) - b.Websocket.DataHandler <- WsFundingInfo{ - YieldLoan: symbolData[0].(float64), - YieldLend: symbolData[1].(float64), - DurationLoan: symbolData[2].(float64), - DurationLend: symbolData[3].(float64), - } - } - } - case wsFundingTradeExecuted, wsFundingTradeUpdate: - if data, ok := chanData[2].([]interface{}); ok && len(data) > 0 { - b.Websocket.DataHandler <- WsFundingTrade{ - ID: int64(data[0].(float64)), - Symbol: data[1].(string), - MTSCreated: int64(data[2].(float64)), - OfferID: int64(data[3].(float64)), - Amount: data[4].(float64), - Rate: data[5].(float64), - Period: int64(data[6].(float64)), - Maker: data[7].(float64) == 1, - } - } - } - } } } } } } +func (b *Bitfinex) wsHandleData(respRaw []byte) error { + var result interface{} + err := json.Unmarshal(respRaw, &result) + if err != nil { + return err + } + switch d := result.(type) { + case map[string]interface{}: + event := d["event"] + switch event { + case "subscribed": + if symbol, ok := d["pair"].(string); ok { + b.WsAddSubscriptionChannel(int(d["chanId"].(float64)), + d["channel"].(string), + symbol, + ) + } else if key, ok := d["key"].(string); ok { + b.WsAddSubscriptionChannel(int(d["chanId"].(float64)), + d["channel"].(string), + key, + ) + } + case "auth": + status := d["status"].(string) + if status == "OK" { + b.Websocket.DataHandler <- d + b.WsAddSubscriptionChannel(0, "account", "N/A") + } else if status == "fail" { + return fmt.Errorf("bitfinex.go error - Websocket unable to AUTH. Error code: %s", + d["code"].(string)) + } + } + case []interface{}: + if hb, ok := d[1].(string); ok { + // Capturing heart beat + if hb == "hb" { + return nil + } + } + chanID := int(d[0].(float64)) + chanInfo, ok := b.WebsocketSubdChannels[chanID] + if !ok && chanID != 0 { + return fmt.Errorf("bitfinex.go error - Unable to locate chanID: %d", + chanID) + } + + switch chanInfo.Channel { + case wsBook: + var newOrderbook []WebsocketBook + curr := currency.NewPairFromString(chanInfo.Pair) + if obSnapBundle, ok := d[1].([]interface{}); ok { + switch id := obSnapBundle[0].(type) { + case []interface{}: + for i := range obSnapBundle { + data := obSnapBundle[i].([]interface{}) + newOrderbook = append(newOrderbook, WebsocketBook{ + ID: int64(data[0].(float64)), + Price: data[1].(float64), + Amount: data[2].(float64)}) + } + err := b.WsInsertSnapshot(curr, + asset.Spot, + newOrderbook) + if err != nil { + return fmt.Errorf("bitfinex_websocket.go inserting snapshot error: %s", + err) + } + case float64: + newOrderbook = append(newOrderbook, WebsocketBook{ + ID: int64(id), + Price: obSnapBundle[1].(float64), + Amount: obSnapBundle[2].(float64)}) + err := b.WsUpdateOrderbook(curr, + asset.Spot, + newOrderbook) + if err != nil { + return fmt.Errorf("bitfinex_websocket.go inserting snapshot error: %s", + err) + } + } + } + return nil + case wsCandles: + curr := currency.NewPairFromString(chanInfo.Pair) + if candleBundle, ok := d[1].([]interface{}); ok { + if len(candleBundle) == 0 { + return nil + } + switch candleData := candleBundle[0].(type) { + case []interface{}: + b.Websocket.DataHandler <- wshandler.KlineData{ + Timestamp: time.Unix(0, int64(candleData[0].(float64))), + Exchange: b.Name, + AssetType: asset.Spot, + Pair: curr, + OpenPrice: candleData[1].(float64), + ClosePrice: candleData[2].(float64), + HighPrice: candleData[3].(float64), + LowPrice: candleData[4].(float64), + Volume: candleData[5].(float64), + } + case float64: + b.Websocket.DataHandler <- wshandler.KlineData{ + Timestamp: time.Unix(0, int64(candleData)), + Exchange: b.Name, + AssetType: asset.Spot, + Pair: curr, + OpenPrice: candleBundle[1].(float64), + ClosePrice: candleBundle[2].(float64), + HighPrice: candleBundle[3].(float64), + LowPrice: candleBundle[4].(float64), + Volume: candleBundle[5].(float64), + } + } + } + return nil + case wsTicker: + tickerData := d[1].([]interface{}) + b.Websocket.DataHandler <- &ticker.Price{ + ExchangeName: b.Name, + Bid: tickerData[0].(float64), + Ask: tickerData[2].(float64), + Last: tickerData[6].(float64), + Volume: tickerData[7].(float64), + High: tickerData[8].(float64), + Low: tickerData[9].(float64), + AssetType: asset.Spot, + Pair: currency.NewPairFromString(chanInfo.Pair), + } + return nil + case wsTrades: + var trades []WebsocketTrade + switch len(d) { + case 2: + snapshot := d[1].([]interface{}) + for i := range snapshot { + elem := snapshot[i].([]interface{}) + if len(elem) == 5 { + trades = append(trades, + WebsocketTrade{ + ID: int64(elem[0].(float64)), + Timestamp: int64(elem[1].(float64)), + Amount: elem[2].(float64), + Rate: elem[3].(float64), + Period: int64(elem[4].(float64)), + }) + } else { + trades = append(trades, + WebsocketTrade{ + ID: int64(elem[0].(float64)), + Timestamp: int64(elem[1].(float64)), + Amount: elem[2].(float64), + Price: elem[3].(float64), + }) + } + } + case 3: + if d[1].(string) == wsTradeExecutionUpdate || + d[1].(string) == wsFundingTradeUpdate { + // "(f)te - trade executed" && "(f)tu - trade updated" + // contain the same amount of data + // "(f)te" gets sent first so we can drop "(f)tu" + return nil + } + data := d[2].([]interface{}) + trades = append(trades, WebsocketTrade{ + ID: int64(data[0].(float64)), + Timestamp: int64(data[1].(float64)), + Price: data[3].(float64), + Amount: data[2].(float64)}) + } + + for i := range trades { + side := order.Buy + newAmount := trades[i].Amount + if newAmount < 0 { + side = order.Sell + newAmount *= -1 + } + + if trades[i].Rate > 0 { + b.Websocket.DataHandler <- wshandler.FundingData{ + CurrencyPair: currency.NewPairFromString(chanInfo.Pair), + Timestamp: time.Unix(0, trades[i].Timestamp*int64(time.Millisecond)), + Amount: newAmount, + Exchange: b.Name, + AssetType: asset.Spot, + Side: side, + Rate: trades[i].Rate, + Period: trades[i].Period, + } + return nil + } + + b.Websocket.DataHandler <- wshandler.TradeData{ + CurrencyPair: currency.NewPairFromString(chanInfo.Pair), + Timestamp: time.Unix(0, trades[i].Timestamp*int64(time.Millisecond)), + Price: trades[i].Price, + Amount: newAmount, + Exchange: b.Name, + AssetType: asset.Spot, + Side: side, + } + } + } + + if authResp, ok := d[1].(string); ok { + switch authResp { + case wsHeartbeat, pong: + return nil + case wsNotification: + notification := d[2].([]interface{}) + if data, ok := notification[4].([]interface{}); ok { + channelName := notification[1].(string) + switch { + case strings.Contains(channelName, wsFundingOrderNewRequest), + strings.Contains(channelName, wsFundingOrderUpdateRequest), + strings.Contains(channelName, wsFundingOrderCancelRequest): + if data[0] != nil && data[0].(float64) > 0 { + id := int64(data[0].(float64)) + if b.WebsocketConn.IsIDWaitingForResponse(id) { + b.AuthenticatedWebsocketConn.SetResponseIDAndData(id, respRaw) + return nil + } + b.wsHandleFundingOffer(data) + } + case strings.Contains(channelName, wsOrderNewRequest), + strings.Contains(channelName, wsOrderUpdateRequest), + strings.Contains(channelName, wsOrderCancelRequest): + if data[2] != nil && data[2].(float64) > 0 { + id := int64(data[2].(float64)) + if b.WebsocketConn.IsIDWaitingForResponse(id) { + b.AuthenticatedWebsocketConn.SetResponseIDAndData(id, respRaw) + return nil + } + b.wsHandleOrder(data) + } + + default: + return fmt.Errorf("%s - Unexpected data returned %s", b.Name, respRaw) + } + } + if notification[5] != nil && strings.EqualFold(notification[5].(string), wsError) { + return fmt.Errorf("%s - Error %s", b.Name, notification[6].(string)) + } + case wsOrderSnapshot: + if snapBundle, ok := d[2].([]interface{}); ok && len(snapBundle) > 0 { + if _, ok := snapBundle[0].([]interface{}); ok { + for i := range snapBundle { + positionData := snapBundle[i].([]interface{}) + b.wsHandleOrder(positionData) + } + } + } + case wsOrderCancel, wsOrderNew, wsOrderUpdate: + if oData, ok := d[2].([]interface{}); ok && len(oData) > 0 { + b.wsHandleOrder(oData) + } + case wsPositionSnapshot: + var snapshot []WebsocketPosition + if snapBundle, ok := d[2].([]interface{}); ok && len(snapBundle) > 0 { + if _, ok := snapBundle[0].([]interface{}); ok { + for i := range snapBundle { + positionData := snapBundle[i].([]interface{}) + position := WebsocketPosition{ + Pair: positionData[0].(string), + Status: positionData[1].(string), + Amount: positionData[2].(float64), + Price: positionData[3].(float64), + MarginFunding: positionData[4].(float64), + MarginFundingType: int64(positionData[5].(float64)), + ProfitLoss: positionData[6].(float64), + ProfitLossPercent: positionData[7].(float64), + LiquidationPrice: positionData[8].(float64), + Leverage: positionData[9].(float64), + } + snapshot = append(snapshot, position) + } + b.Websocket.DataHandler <- snapshot + } + } + case wsPositionNew, wsPositionUpdate, wsPositionClose: + if positionData, ok := d[2].([]interface{}); ok && len(positionData) > 0 { + position := WebsocketPosition{ + Pair: positionData[0].(string), + Status: positionData[1].(string), + Amount: positionData[2].(float64), + Price: positionData[3].(float64), + MarginFunding: positionData[4].(float64), + MarginFundingType: int64(positionData[5].(float64)), + ProfitLoss: positionData[6].(float64), + ProfitLossPercent: positionData[7].(float64), + LiquidationPrice: positionData[8].(float64), + Leverage: positionData[9].(float64), + } + b.Websocket.DataHandler <- position + } + case wsTradeExecuted, wsTradeExecutionUpdate: + if tradeData, ok := d[2].([]interface{}); ok && len(tradeData) > 4 { + b.Websocket.DataHandler <- WebsocketTradeData{ + TradeID: int64(tradeData[0].(float64)), + Pair: tradeData[1].(string), + Timestamp: int64(tradeData[2].(float64)), + OrderID: int64(tradeData[3].(float64)), + AmountExecuted: tradeData[4].(float64), + PriceExecuted: tradeData[5].(float64), + OrderType: tradeData[6].(string), + OrderPrice: tradeData[7].(float64), + Maker: tradeData[8].(float64) == 1, + Fee: tradeData[9].(float64), + FeeCurrency: tradeData[10].(string), + } + } + case wsFundingOrderSnapshot: + var snapshot []WsFundingOffer + if snapBundle, ok := d[2].([]interface{}); ok && len(snapBundle) > 0 { + if _, ok := snapBundle[0].([]interface{}); ok { + for i := range snapBundle { + data := snapBundle[i].([]interface{}) + offer := WsFundingOffer{ + ID: int64(data[0].(float64)), + Symbol: data[1].(string), + Created: int64(data[2].(float64)), + Updated: int64(data[3].(float64)), + Amount: data[4].(float64), + OriginalAmount: data[5].(float64), + Type: data[6].(string), + Flags: data[9].(float64), + Status: data[10].(string), + Rate: data[14].(float64), + Period: int64(data[15].(float64)), + Notify: data[16].(float64) == 1, + Hidden: data[17].(float64) == 1, + Insure: data[18].(float64) == 1, + Renew: data[19].(float64) == 1, + RateReal: data[20].(float64), + } + snapshot = append(snapshot, offer) + } + b.Websocket.DataHandler <- snapshot + } + } + case wsFundingOrderNew, wsFundingOrderUpdate, wsFundingOrderCancel: + if data, ok := d[2].([]interface{}); ok && len(data) > 0 { + b.wsHandleFundingOffer(data) + } + case wsFundingCreditSnapshot: + var snapshot []WsCredit + if snapBundle, ok := d[2].([]interface{}); ok && len(snapBundle) > 0 { + if _, ok := snapBundle[0].([]interface{}); ok { + for i := range snapBundle { + data := snapBundle[i].([]interface{}) + credit := WsCredit{ + ID: int64(data[0].(float64)), + Symbol: data[1].(string), + Side: data[2].(string), + Created: int64(data[3].(float64)), + Updated: int64(data[4].(float64)), + Amount: data[5].(float64), + Flags: data[6].(string), + Status: data[7].(string), + Rate: data[11].(float64), + Period: int64(data[12].(float64)), + Opened: int64(data[13].(float64)), + LastPayout: int64(data[14].(float64)), + Notify: data[15].(float64) == 1, + Hidden: data[16].(float64) == 1, + Insure: data[17].(float64) == 1, + Renew: data[18].(float64) == 1, + RateReal: data[19].(float64), + NoClose: data[20].(float64) == 1, + PositionPair: data[21].(string), + } + snapshot = append(snapshot, credit) + } + b.Websocket.DataHandler <- snapshot + } + } + case wsFundingCreditNew, wsFundingCreditUpdate, wsFundingCreditCancel: + if data, ok := d[2].([]interface{}); ok && len(data) > 0 { + b.Websocket.DataHandler <- WsCredit{ + ID: int64(data[0].(float64)), + Symbol: data[1].(string), + Side: data[2].(string), + Created: int64(data[3].(float64)), + Updated: int64(data[4].(float64)), + Amount: data[5].(float64), + Flags: data[6].(string), + Status: data[7].(string), + Rate: data[11].(float64), + Period: int64(data[12].(float64)), + Opened: int64(data[13].(float64)), + LastPayout: int64(data[14].(float64)), + Notify: data[15].(float64) == 1, + Hidden: data[16].(float64) == 1, + Insure: data[17].(float64) == 1, + Renew: data[18].(float64) == 1, + RateReal: data[19].(float64), + NoClose: data[20].(float64) == 1, + PositionPair: data[21].(string), + } + } + case wsFundingLoanSnapshot: + var snapshot []WsCredit + if snapBundle, ok := d[2].([]interface{}); ok && len(snapBundle) > 0 { + if _, ok := snapBundle[0].([]interface{}); ok { + for i := range snapBundle { + data := snapBundle[i].([]interface{}) + credit := WsCredit{ + ID: int64(data[0].(float64)), + Symbol: data[1].(string), + Side: data[2].(string), + Created: int64(data[3].(float64)), + Updated: int64(data[4].(float64)), + Amount: data[5].(float64), + Flags: data[6].(string), + Status: data[7].(string), + Rate: data[11].(float64), + Period: int64(data[12].(float64)), + Opened: int64(data[13].(float64)), + LastPayout: int64(data[14].(float64)), + Notify: data[15].(float64) == 1, + Hidden: data[16].(float64) == 1, + Insure: data[17].(float64) == 1, + Renew: data[18].(float64) == 1, + RateReal: data[19].(float64), + NoClose: data[20].(float64) == 1, + } + snapshot = append(snapshot, credit) + } + b.Websocket.DataHandler <- snapshot + } + } + case wsFundingLoanNew, wsFundingLoanUpdate, wsFundingLoanCancel: + if data, ok := d[2].([]interface{}); ok && len(data) > 0 { + b.Websocket.DataHandler <- WsCredit{ + ID: int64(data[0].(float64)), + Symbol: data[1].(string), + Side: data[2].(string), + Created: int64(data[3].(float64)), + Updated: int64(data[4].(float64)), + Amount: data[5].(float64), + Flags: data[6].(string), + Status: data[7].(string), + Rate: data[11].(float64), + Period: int64(data[12].(float64)), + Opened: int64(data[13].(float64)), + LastPayout: int64(data[14].(float64)), + Notify: data[15].(float64) == 1, + Hidden: data[16].(float64) == 1, + Insure: data[17].(float64) == 1, + Renew: data[18].(float64) == 1, + RateReal: data[19].(float64), + NoClose: data[20].(float64) == 1, + } + } + case wsWalletSnapshot: + var snapshot []WsWallet + if snapBundle, ok := d[2].([]interface{}); ok && len(snapBundle) > 0 { + if _, ok := snapBundle[0].([]interface{}); ok { + for i := range snapBundle { + data := snapBundle[i].([]interface{}) + var balanceAvailable float64 + if _, ok := data[4].(float64); ok { + balanceAvailable = data[4].(float64) + } + wallet := WsWallet{ + Type: data[0].(string), + Currency: data[1].(string), + Balance: data[2].(float64), + UnsettledInterest: data[3].(float64), + BalanceAvailable: balanceAvailable, + } + snapshot = append(snapshot, wallet) + } + b.Websocket.DataHandler <- snapshot + } + } + case wsWalletUpdate: + if data, ok := d[2].([]interface{}); ok && len(data) > 0 { + var balanceAvailable float64 + if _, ok := data[4].(float64); ok { + balanceAvailable = data[4].(float64) + } + b.Websocket.DataHandler <- WsWallet{ + Type: data[0].(string), + Currency: data[1].(string), + Balance: data[2].(float64), + UnsettledInterest: data[3].(float64), + BalanceAvailable: balanceAvailable, + } + } + case wsBalanceUpdate: + if data, ok := d[2].([]interface{}); ok && len(data) > 0 { + b.Websocket.DataHandler <- WsBalanceInfo{ + TotalAssetsUnderManagement: data[0].(float64), + NetAssetsUnderManagement: data[1].(float64), + } + } + case wsMarginInfoUpdate: + if data, ok := d[2].([]interface{}); ok && len(data) > 0 { + if data[0].(string) == "base" { + if infoBase, ok := d[2].([]interface{}); ok && len(infoBase) > 0 { + baseData := data[1].([]interface{}) + b.Websocket.DataHandler <- WsMarginInfoBase{ + UserProfitLoss: baseData[0].(float64), + UserSwaps: baseData[1].(float64), + MarginBalance: baseData[2].(float64), + MarginNet: baseData[3].(float64), + } + } + } + } + case wsFundingInfoUpdate: + if data, ok := d[2].([]interface{}); ok && len(data) > 0 { + if data[0].(string) == "sym" { + symbolData := data[1].([]interface{}) + b.Websocket.DataHandler <- WsFundingInfo{ + YieldLoan: symbolData[0].(float64), + YieldLend: symbolData[1].(float64), + DurationLoan: symbolData[2].(float64), + DurationLend: symbolData[3].(float64), + } + } + } + case wsFundingTradeExecuted, wsFundingTradeUpdate: + if data, ok := d[2].([]interface{}); ok && len(data) > 0 { + b.Websocket.DataHandler <- WsFundingTrade{ + ID: int64(data[0].(float64)), + Symbol: data[1].(string), + MTSCreated: int64(data[2].(float64)), + OfferID: int64(data[3].(float64)), + Amount: data[4].(float64), + Rate: data[5].(float64), + Period: int64(data[6].(float64)), + Maker: data[7].(float64) == 1, + } + } + default: + b.Websocket.DataHandler <- wshandler.UnhandledMessageWarning{Message: b.Name + wshandler.UnhandledMessage + string(respRaw)} + return nil + } + } + } + return nil +} + +func (b *Bitfinex) wsHandleFundingOffer(data []interface{}) { + var fo WsFundingOffer + if data[0] != nil { + fo.ID = int64(data[0].(float64)) + } + if data[1] != nil { + fo.Symbol = data[1].(string)[1:] + } + if data[2] != nil { + fo.Created = int64(data[2].(float64)) + } + if data[3] != nil { + fo.Updated = int64(data[0].(float64)) + } + if data[15] != nil { + fo.Period = int64(data[15].(float64)) + } + if data[4] != nil { + fo.Amount = data[4].(float64) + } + if data[5] != nil { + fo.OriginalAmount = data[5].(float64) + } + if data[6] != nil { + fo.Type = data[6].(string) + } + if data[9] != nil { + fo.Flags = data[9].(float64) + } + if data[9] != nil { + fo.Status = data[10].(string) + } + if data[9] != nil { + fo.Rate = data[14].(float64) + } + if data[16] != nil { + fo.Notify = data[16].(float64) == 1 + } + if data[17] != nil { + fo.Hidden = data[17].(float64) == 1 + } + if data[18] != nil { + fo.Insure = data[18].(float64) == 1 + } + if data[19] != nil { + fo.Renew = data[19].(float64) == 1 + } + if data[20] != nil { + fo.RateReal = data[20].(float64) + } + + b.Websocket.DataHandler <- fo +} + +func (b *Bitfinex) wsHandleOrder(data []interface{}) { + var od order.Detail + var err error + od.Exchange = b.Name + if data[0] != nil { + od.ID = strconv.FormatFloat(data[0].(float64), 'f', -1, 64) + } + if data[16] != nil { + od.Price = data[16].(float64) + } + if data[7] != nil { + od.Amount = data[7].(float64) + } + if data[6] != nil { + od.RemainingAmount = data[6].(float64) + } + if data[7] != nil && data[6] != nil { + od.ExecutedAmount = data[7].(float64) - data[6].(float64) + } + if data[4] != nil { + od.Date = time.Unix(int64(data[4].(float64))*1000, 0) + } + if data[5] != nil { + od.LastUpdated = time.Unix(int64(data[5].(float64))*1000, 0) + } + if data[2] != nil { + od.Pair, od.AssetType, err = b.GetRequestFormattedPairAndAssetType(data[3].(string)[1:]) + if err != nil { + b.Websocket.DataHandler <- err + return + } + } + if data[8] != nil { + oType, err := order.StringToOrderType(data[8].(string)) + if err != nil { + b.Websocket.DataHandler <- order.ClassificationError{ + Exchange: b.Name, + OrderID: od.ID, + Err: err, + } + } + od.Type = oType + } + if data[13] != nil { + oStatus, err := order.StringToOrderStatus(data[13].(string)) + if err != nil { + b.Websocket.DataHandler <- order.ClassificationError{ + Exchange: b.Name, + OrderID: od.ID, + Err: err, + } + } + od.Status = oStatus + } + b.Websocket.DataHandler <- &od +} + // WsInsertSnapshot add the initial orderbook snapshot when subscribed to a // channel func (b *Bitfinex) WsInsertSnapshot(p currency.Pair, assetType asset.Item, books []WebsocketBook) error { diff --git a/exchanges/bitfinex/bitfinex_wrapper.go b/exchanges/bitfinex/bitfinex_wrapper.go index aa96bc99..45f90116 100644 --- a/exchanges/bitfinex/bitfinex_wrapper.go +++ b/exchanges/bitfinex/bitfinex_wrapper.go @@ -113,6 +113,8 @@ func (b *Bitfinex) SetDefaults() { AuthenticatedEndpoints: true, MessageCorrelation: true, DeadMansSwitch: true, + GetOrders: true, + GetOrder: true, }, WithdrawPermissions: exchange.AutoWithdrawCryptoWithAPIPermission | exchange.AutoWithdrawFiatWithAPIPermission, @@ -436,7 +438,7 @@ func (b *Bitfinex) SubmitOrder(o *order.Submit) (order.SubmitResponse, error) { if b.Websocket.CanUseAuthenticatedWebsocketForWrapper() { submitOrderResponse.OrderID, err = b.WsNewOrder(&WsNewOrderRequest{ CustomID: b.AuthenticatedWebsocketConn.GenerateMessageID(false), - Type: o.OrderType.String(), + Type: o.Type.String(), Symbol: b.FormatExchangeCurrency(o.Pair, asset.Spot).String(), Amount: o.Amount, Price: o.Price, @@ -446,10 +448,10 @@ func (b *Bitfinex) SubmitOrder(o *order.Submit) (order.SubmitResponse, error) { } } else { var response Order - isBuying := o.OrderSide == order.Buy + isBuying := o.Side == order.Buy b.appendOptionalDelimiter(&o.Pair) response, err = b.NewOrder(o.Pair.String(), - o.OrderType.String(), + o.Type.String(), o.Amount, o.Price, false, @@ -472,9 +474,9 @@ func (b *Bitfinex) SubmitOrder(o *order.Submit) (order.SubmitResponse, error) { // ModifyOrder will allow of changing orderbook placement and limit to // market conversion func (b *Bitfinex) ModifyOrder(action *order.Modify) (string, error) { - orderIDInt, err := strconv.ParseInt(action.OrderID, 10, 64) + orderIDInt, err := strconv.ParseInt(action.ID, 10, 64) if err != nil { - return action.OrderID, err + return action.ID, err } if b.Websocket.CanUseAuthenticatedWebsocketForWrapper() { if action.Side == order.Sell && action.Amount > 0 { @@ -485,14 +487,14 @@ func (b *Bitfinex) ModifyOrder(action *order.Modify) (string, error) { Price: action.Price, Amount: action.Amount, }) - return action.OrderID, err + return action.ID, err } return "", common.ErrNotYetImplemented } // CancelOrder cancels an order by its corresponding ID number func (b *Bitfinex) CancelOrder(order *order.Cancel) error { - orderIDInt, err := strconv.ParseInt(order.OrderID, 10, 64) + orderIDInt, err := strconv.ParseInt(order.ID, 10, 64) if err != nil { return err } @@ -623,13 +625,13 @@ func (b *Bitfinex) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, orderDetail := order.Detail{ Amount: resp[i].OriginalAmount, - OrderDate: orderDate, + Date: orderDate, Exchange: b.Name, ID: strconv.FormatInt(resp[i].OrderID, 10), - OrderSide: orderSide, + Side: orderSide, Price: resp[i].Price, RemainingAmount: resp[i].RemainingAmount, - CurrencyPair: currency.NewPairFromString(resp[i].Symbol), + Pair: currency.NewPairFromString(resp[i].Symbol), ExecutedAmount: resp[i].ExecutedAmount, } @@ -648,18 +650,18 @@ func (b *Bitfinex) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, // Return type suggests “market” / “limit” / “stop” / “trailing-stop” orderType := strings.Replace(resp[i].Type, "exchange ", "", 1) if orderType == "trailing-stop" { - orderDetail.OrderType = order.TrailingStop + orderDetail.Type = order.TrailingStop } else { - orderDetail.OrderType = order.Type(strings.ToUpper(orderType)) + orderDetail.Type = order.Type(strings.ToUpper(orderType)) } orders = append(orders, orderDetail) } - order.FilterOrdersBySide(&orders, req.OrderSide) - order.FilterOrdersByType(&orders, req.OrderType) + order.FilterOrdersBySide(&orders, req.Side) + order.FilterOrdersByType(&orders, req.Type) order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) - order.FilterOrdersByCurrencies(&orders, req.Currencies) + order.FilterOrdersByCurrencies(&orders, req.Pairs) return orders, nil } @@ -682,14 +684,14 @@ func (b *Bitfinex) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail, orderDetail := order.Detail{ Amount: resp[i].OriginalAmount, - OrderDate: orderDate, + Date: orderDate, Exchange: b.Name, ID: strconv.FormatInt(resp[i].OrderID, 10), - OrderSide: orderSide, + Side: orderSide, Price: resp[i].Price, RemainingAmount: resp[i].RemainingAmount, ExecutedAmount: resp[i].ExecutedAmount, - CurrencyPair: currency.NewPairFromString(resp[i].Symbol), + Pair: currency.NewPairFromString(resp[i].Symbol), } switch { @@ -707,21 +709,21 @@ func (b *Bitfinex) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail, // Return type suggests “market” / “limit” / “stop” / “trailing-stop” orderType := strings.Replace(resp[i].Type, "exchange ", "", 1) if orderType == "trailing-stop" { - orderDetail.OrderType = order.TrailingStop + orderDetail.Type = order.TrailingStop } else { - orderDetail.OrderType = order.Type(strings.ToUpper(orderType)) + orderDetail.Type = order.Type(strings.ToUpper(orderType)) } orders = append(orders, orderDetail) } - order.FilterOrdersBySide(&orders, req.OrderSide) - order.FilterOrdersByType(&orders, req.OrderType) + order.FilterOrdersBySide(&orders, req.Side) + order.FilterOrdersByType(&orders, req.Type) order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) - for i := range req.Currencies { - b.appendOptionalDelimiter(&req.Currencies[i]) + for i := range req.Pairs { + b.appendOptionalDelimiter(&req.Pairs[i]) } - order.FilterOrdersByCurrencies(&orders, req.Currencies) + order.FilterOrdersByCurrencies(&orders, req.Pairs) return orders, nil } diff --git a/exchanges/bitflyer/bitflyer_test.go b/exchanges/bitflyer/bitflyer_test.go index 80d0d6d2..01718bdd 100644 --- a/exchanges/bitflyer/bitflyer_test.go +++ b/exchanges/bitflyer/bitflyer_test.go @@ -268,7 +268,7 @@ func TestFormatWithdrawPermissions(t *testing.T) { func TestGetActiveOrders(t *testing.T) { t.Parallel() var getOrdersRequest = order.GetOrdersRequest{ - OrderType: order.AnyType, + Type: order.AnyType, } _, err := b.GetActiveOrders(&getOrdersRequest) @@ -282,7 +282,7 @@ func TestGetActiveOrders(t *testing.T) { func TestGetOrderHistory(t *testing.T) { t.Parallel() var getOrdersRequest = order.GetOrdersRequest{ - OrderType: order.AnyType, + Type: order.AnyType, } _, err := b.GetOrderHistory(&getOrdersRequest) @@ -308,11 +308,11 @@ func TestSubmitOrder(t *testing.T) { Base: currency.BTC, Quote: currency.LTC, }, - OrderSide: order.Buy, - OrderType: order.Limit, - Price: 1, - Amount: 1, - ClientID: "meowOrder", + Side: order.Buy, + Type: order.Limit, + Price: 1, + Amount: 1, + ClientID: "meowOrder", } _, err := b.SubmitOrder(orderSubmission) if err != common.ErrNotYetImplemented { @@ -328,10 +328,10 @@ func TestCancelExchangeOrder(t *testing.T) { currencyPair := currency.NewPair(currency.LTC, currency.BTC) var orderCancellation = &order.Cancel{ - OrderID: "1", + ID: "1", WalletAddress: core.BitcoinDonationAddress, AccountID: "1", - CurrencyPair: currencyPair, + Pair: currencyPair, } err := b.CancelOrder(orderCancellation) @@ -349,10 +349,10 @@ func TestCancelAllExchangeOrders(t *testing.T) { currencyPair := currency.NewPair(currency.LTC, currency.BTC) var orderCancellation = &order.Cancel{ - OrderID: "1", + ID: "1", WalletAddress: core.BitcoinDonationAddress, AccountID: "1", - CurrencyPair: currencyPair, + Pair: currencyPair, } _, err := b.CancelAllOrders(orderCancellation) diff --git a/exchanges/bithumb/bithumb_test.go b/exchanges/bithumb/bithumb_test.go index 32f0bb78..8a88d8d5 100644 --- a/exchanges/bithumb/bithumb_test.go +++ b/exchanges/bithumb/bithumb_test.go @@ -308,8 +308,8 @@ func TestFormatWithdrawPermissions(t *testing.T) { func TestGetActiveOrders(t *testing.T) { t.Parallel() var getOrdersRequest = order.GetOrdersRequest{ - OrderType: order.AnyType, - OrderSide: order.Sell, + Type: order.AnyType, + Side: order.Sell, } _, err := b.GetActiveOrders(&getOrdersRequest) @@ -323,7 +323,7 @@ func TestGetActiveOrders(t *testing.T) { func TestGetOrderHistory(t *testing.T) { t.Parallel() var getOrdersRequest = order.GetOrdersRequest{ - OrderType: order.AnyType, + Type: order.AnyType, } _, err := b.GetOrderHistory(&getOrdersRequest) @@ -351,11 +351,11 @@ func TestSubmitOrder(t *testing.T) { Base: currency.BTC, Quote: currency.LTC, }, - OrderSide: order.Buy, - OrderType: order.Limit, - Price: 1, - Amount: 1, - ClientID: "meowOrder", + Side: order.Buy, + Type: order.Limit, + Price: 1, + Amount: 1, + ClientID: "meowOrder", } response, err := b.SubmitOrder(orderSubmission) if areTestAPIKeysSet() && (err != nil || !response.IsOrderPlaced) { @@ -373,10 +373,10 @@ func TestCancelExchangeOrder(t *testing.T) { currencyPair := currency.NewPair(currency.LTC, currency.BTC) var orderCancellation = &order.Cancel{ - OrderID: "1", + ID: "1", WalletAddress: core.BitcoinDonationAddress, AccountID: "1", - CurrencyPair: currencyPair, + Pair: currencyPair, } err := b.CancelOrder(orderCancellation) @@ -396,10 +396,10 @@ func TestCancelAllExchangeOrders(t *testing.T) { currencyPair := currency.NewPair(currency.LTC, currency.BTC) var orderCancellation = &order.Cancel{ - OrderID: "1", + ID: "1", WalletAddress: core.BitcoinDonationAddress, AccountID: "1", - CurrencyPair: currencyPair, + Pair: currencyPair, } resp, err := b.CancelAllOrders(orderCancellation) @@ -435,11 +435,11 @@ func TestModifyOrder(t *testing.T) { t.Parallel() curr := currency.NewPairFromString("BTCUSD") _, err := b.ModifyOrder(&order.Modify{ - OrderID: "1337", - Price: 100, - Amount: 1000, - Side: order.Sell, - CurrencyPair: curr}) + ID: "1337", + Price: 100, + Amount: 1000, + Side: order.Sell, + Pair: curr}) if err == nil { t.Error("ModifyOrder() Expected error") } diff --git a/exchanges/bithumb/bithumb_wrapper.go b/exchanges/bithumb/bithumb_wrapper.go index 14c28647..6fb120bc 100644 --- a/exchanges/bithumb/bithumb_wrapper.go +++ b/exchanges/bithumb/bithumb_wrapper.go @@ -325,14 +325,14 @@ func (b *Bithumb) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) { var orderID string var err error - if s.OrderSide == order.Buy { + if s.Side == order.Buy { var result MarketBuy result, err = b.MarketBuyOrder(s.Pair.Base.String(), s.Amount) if err != nil { return submitOrderResponse, err } orderID = result.OrderID - } else if s.OrderSide == order.Sell { + } else if s.Side == order.Sell { var result MarketSell result, err = b.MarketSellOrder(s.Pair.Base.String(), s.Amount) if err != nil { @@ -352,8 +352,8 @@ func (b *Bithumb) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) { // ModifyOrder will allow of changing orderbook placement and limit to // market conversion func (b *Bithumb) ModifyOrder(action *order.Modify) (string, error) { - order, err := b.ModifyTrade(action.OrderID, - action.CurrencyPair.Base.String(), + order, err := b.ModifyTrade(action.ID, + action.Pair.Base.String(), action.Side.Lower(), action.Amount, int64(action.Price)) @@ -368,8 +368,8 @@ func (b *Bithumb) ModifyOrder(action *order.Modify) (string, error) { // CancelOrder cancels an order by its corresponding ID number func (b *Bithumb) CancelOrder(order *order.Cancel) error { _, err := b.CancelTrade(order.Side.String(), - order.OrderID, - order.CurrencyPair.Base.String()) + order.ID, + order.Pair.Base.String()) return err } @@ -396,7 +396,7 @@ func (b *Bithumb) CancelAllOrders(orderCancellation *order.Cancel) (order.Cancel for i := range allOrders { _, err := b.CancelTrade(orderCancellation.Side.String(), allOrders[i].OrderID, - orderCancellation.CurrencyPair.Base.String()) + orderCancellation.Pair.Base.String()) if err != nil { cancelAllOrdersResponse.Status[allOrders[i].OrderID] = err.Error() } @@ -498,27 +498,27 @@ func (b *Bithumb) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, Amount: resp.Data[i].Units, Exchange: b.Name, ID: resp.Data[i].OrderID, - OrderDate: orderDate, + Date: orderDate, Price: resp.Data[i].Price, RemainingAmount: resp.Data[i].UnitsRemaining, Status: order.Active, - CurrencyPair: currency.NewPairWithDelimiter(resp.Data[i].OrderCurrency, + Pair: currency.NewPairWithDelimiter(resp.Data[i].OrderCurrency, resp.Data[i].PaymentCurrency, b.GetPairFormat(asset.Spot, false).Delimiter), } if resp.Data[i].Type == "bid" { - orderDetail.OrderSide = order.Buy + orderDetail.Side = order.Buy } else if resp.Data[i].Type == "ask" { - orderDetail.OrderSide = order.Sell + orderDetail.Side = order.Sell } orders = append(orders, orderDetail) } - order.FilterOrdersBySide(&orders, req.OrderSide) + order.FilterOrdersBySide(&orders, req.Side) order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) - order.FilterOrdersByCurrencies(&orders, req.Currencies) + order.FilterOrdersByCurrencies(&orders, req.Pairs) return orders, nil } @@ -541,26 +541,26 @@ func (b *Bithumb) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail, Amount: resp.Data[i].Units, Exchange: b.Name, ID: resp.Data[i].OrderID, - OrderDate: orderDate, + Date: orderDate, Price: resp.Data[i].Price, RemainingAmount: resp.Data[i].UnitsRemaining, - CurrencyPair: currency.NewPairWithDelimiter(resp.Data[i].OrderCurrency, + Pair: currency.NewPairWithDelimiter(resp.Data[i].OrderCurrency, resp.Data[i].PaymentCurrency, b.GetPairFormat(asset.Spot, false).Delimiter), } if resp.Data[i].Type == "bid" { - orderDetail.OrderSide = order.Buy + orderDetail.Side = order.Buy } else if resp.Data[i].Type == "ask" { - orderDetail.OrderSide = order.Sell + orderDetail.Side = order.Sell } orders = append(orders, orderDetail) } - order.FilterOrdersBySide(&orders, req.OrderSide) + order.FilterOrdersBySide(&orders, req.Side) order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) - order.FilterOrdersByCurrencies(&orders, req.Currencies) + order.FilterOrdersByCurrencies(&orders, req.Pairs) return orders, nil } diff --git a/exchanges/bitmex/bitmex_parameters.go b/exchanges/bitmex/bitmex_parameters.go index d1beda15..e158d954 100644 --- a/exchanges/bitmex/bitmex_parameters.go +++ b/exchanges/bitmex/bitmex_parameters.go @@ -258,38 +258,38 @@ func (p LeaderboardGetParams) IsNil() bool { // OrderNewParams contains all the parameters to send to the API endpoint type OrderNewParams struct { - // ClOrdID - [Optional] Client Order ID. This clOrdID will come back on the + // ClientOrderID - [Optional] Client Order ID. This clOrdID will come back on the // order and any related executions. - ClOrdID string `json:"clOrdID,omitempty"` + ClientOrderID string `json:"clOrdID,omitempty"` - // ClOrdLinkID - [Optional] Client Order Link ID for contingent orders. - ClOrdLinkID string `json:"clOrdLinkID,omitempty"` + // ClientOrderLinkID - [Optional] Client Order Link ID for contingent orders. + ClientOrderLinkID string `json:"clOrdLinkID,omitempty"` // ContingencyType - [Optional] contingency type for use with `clOrdLinkID`. // Valid options: OneCancelsTheOther, OneTriggersTheOther, // OneUpdatesTheOtherAbsolute, OneUpdatesTheOtherProportional. ContingencyType string `json:"contingencyType,omitempty"` - // DisplayQty - [Optional] quantity to display in the book. Use 0 for a fully + // DisplayQuantity- [Optional] quantity to display in the book. Use 0 for a fully // hidden order. - DisplayQty float64 `json:"displayQty,omitempty"` + DisplayQuantity float64 `json:"displayQty,omitempty"` - // ExecInst - [Optional] execution instructions. Valid options: + // ExecutionInstance - [Optional] execution instructions. Valid options: // ParticipateDoNotInitiate, AllOrNone, MarkPrice, IndexPrice, LastPrice, // Close, ReduceOnly, Fixed. 'AllOrNone' instruction requires `displayQty` // to be 0. 'MarkPrice', 'IndexPrice' or 'LastPrice' instruction valid for // 'Stop', 'StopLimit', 'MarketIfTouched', and 'LimitIfTouched' orders. ExecInst string `json:"execInst,omitempty"` - // OrdType - Order type. Valid options: Market, Limit, Stop, StopLimit, + // OrderType - Order type. Valid options: Market, Limit, Stop, StopLimit, // MarketIfTouched, LimitIfTouched, MarketWithLeftOverAsLimit, Pegged. // Defaults to 'Limit' when `price` is specified. Defaults to 'Stop' when // `stopPx` is specified. Defaults to 'StopLimit' when `price` and `stopPx` // are specified. - OrdType string `json:"ordType,omitempty"` + OrderType string `json:"ordType,omitempty"` - // OrderQty Order quantity in units of the instrument (i.e. contracts). - OrderQty float64 `json:"orderQty,omitempty"` + // OrderQuantity Order quantity in units of the instrument (i.e. contracts). + OrderQuantity float64 `json:"orderQty,omitempty"` // PegOffsetValue - [Optional] trailing offset from the current price for // 'Stop', 'StopLimit', 'MarketIfTouched', and 'LimitIfTouched' orders; use a @@ -309,11 +309,11 @@ type OrderNewParams struct { // `orderQty` or `simpleOrderQty` is negative. Side string `json:"side,omitempty"` - // SimpleOrderQty - Order quantity in units of the underlying instrument + // SimpleOrderQuantity - Order quantity in units of the underlying instrument // (i.e. Bitcoin). - SimpleOrderQty float64 `json:"simpleOrderQty,omitempty"` + SimpleOrderQuantity float64 `json:"simpleOrderQty,omitempty"` - // StopPx - [Optional] trigger price for 'Stop', 'StopLimit', + // StopPrice - [Optional] trigger price for 'Stop', 'StopLimit', // 'MarketIfTouched', and 'LimitIfTouched' orders. Use a price below the // current price for stop-sell orders and buy-if-touched orders. Use // `execInst` of 'MarkPrice' or 'LastPrice' to define the current price used @@ -351,16 +351,16 @@ func (p *OrderNewParams) IsNil() bool { // OrderAmendParams contains all the parameters to send to the API endpoint // for the order amend operation type OrderAmendParams struct { - // ClOrdID - [Optional] new Client Order ID, requires `origClOrdID`. - ClOrdID string `json:"clOrdID,omitempty"` + // ClientOrderID - [Optional] new Client Order ID, requires `origClOrdID`. + ClientOrderID string `json:"clOrdID,omitempty"` - // LeavesQty - [Optional] leaves quantity in units of the instrument + // LeavesQuantity - [Optional] leaves quantity in units of the instrument // (i.e. contracts). Useful for amending partially filled orders. - LeavesQty int32 `json:"leavesQty,omitempty"` + LeavesQuantity int32 `json:"leavesQty,omitempty"` OrderID string `json:"orderID,omitempty"` - // OrderQty - [Optional] order quantity in units of the instrument + // OrderQuantity - [Optional] order quantity in units of the instrument // (i.e. contracts). OrderQty int32 `json:"orderQty,omitempty"` @@ -377,15 +377,15 @@ type OrderAmendParams struct { // 'LimitIfTouched' orders. Price float64 `json:"price,omitempty"` - // SimpleLeavesQty - [Optional] leaves quantity in units of the underlying + // SimpleLeavesQuantity - [Optional] leaves quantity in units of the underlying // instrument (i.e. Bitcoin). Useful for amending partially filled orders. - SimpleLeavesQty float64 `json:"simpleLeavesQty,omitempty"` + SimpleLeavesQuantity float64 `json:"simpleLeavesQty,omitempty"` - // SimpleOrderQty - [Optional] order quantity in units of the underlying + // SimpleOrderQuantity - [Optional] order quantity in units of the underlying // instrument (i.e. Bitcoin). - SimpleOrderQty float64 `json:"simpleOrderQty,omitempty"` + SimpleOrderQuantity float64 `json:"simpleOrderQty,omitempty"` - // StopPx - [Optional] trigger price for 'Stop', 'StopLimit', + // StopPrice - [Optional] trigger price for 'Stop', 'StopLimit', // 'MarketIfTouched', and 'LimitIfTouched' orders. Use a price below the // current price for stop-sell orders and buy-if-touched orders. StopPx float64 `json:"stopPx,omitempty"` @@ -397,7 +397,7 @@ type OrderAmendParams struct { // VerifyData verifies outgoing data sets func (p *OrderAmendParams) VerifyData() error { if p.OrderID == "" { - return errors.New("verifydata() OrderNewParams error - OrderID not set") + return errors.New("verifydata() OrderNewParams error - ID not set") } return nil } @@ -415,8 +415,8 @@ func (p *OrderAmendParams) IsNil() bool { // OrderCancelParams contains all the parameters to send to the API endpoint type OrderCancelParams struct { - // ClOrdID - Client Order ID(s). See POST /order. - ClOrdID string `json:"clOrdID,omitempty"` + // ClientOrderID - Client Order ID(s). See POST /order. + ClientOrderID string `json:"clOrdID,omitempty"` // OrderID - Order ID(s). OrderID string `json:"orderID,omitempty"` diff --git a/exchanges/bitmex/bitmex_test.go b/exchanges/bitmex/bitmex_test.go index fde5af04..8a5b8a18 100644 --- a/exchanges/bitmex/bitmex_test.go +++ b/exchanges/bitmex/bitmex_test.go @@ -50,6 +50,8 @@ func TestMain(m *testing.M) { if err != nil { log.Fatal("Bitmex setup error", err) } + b.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride() + b.Websocket.TrafficAlert = sharedtestvalues.GetWebsocketStructChannelOverride() os.Exit(m.Run()) } @@ -231,9 +233,9 @@ func TestAmendOrder(t *testing.T) { func TestCreateOrder(t *testing.T) { _, err := b.CreateOrder(&OrderNewParams{Symbol: "XBTM15", - Price: 219.0, - ClOrdID: "mm_bitmex_1a/oemUeQ4CAJZgP3fjHsA", - OrderQty: 98}) + Price: 219.0, + ClientOrderID: "mm_bitmex_1a/oemUeQ4CAJZgP3fjHsA", + OrderQuantity: 98}) if err == nil { t.Error("CreateOrder() Expected error") } @@ -360,7 +362,7 @@ func TestGetStatSummary(t *testing.T) { func TestGetTrade(t *testing.T) { _, err := b.GetTrade(&GenericRequestParams{ - Symbol: "XBTUSD", + Symbol: "ETHUSD", StartTime: time.Now().Format(time.RFC3339), Reverse: true}) if err != nil { @@ -478,7 +480,7 @@ func TestFormatWithdrawPermissions(t *testing.T) { func TestGetActiveOrders(t *testing.T) { var getOrdersRequest = order.GetOrdersRequest{ - OrderType: order.AnyType, + Type: order.AnyType, } _, err := b.GetActiveOrders(&getOrdersRequest) @@ -491,8 +493,8 @@ func TestGetActiveOrders(t *testing.T) { func TestGetOrderHistory(t *testing.T) { var getOrdersRequest = order.GetOrdersRequest{ - OrderType: order.AnyType, - Currencies: []currency.Pair{currency.NewPair(currency.LTC, + Type: order.AnyType, + Pairs: []currency.Pair{currency.NewPair(currency.LTC, currency.BTC)}, } @@ -520,11 +522,11 @@ func TestSubmitOrder(t *testing.T) { Base: currency.XBT, Quote: currency.USD, }, - OrderSide: order.Buy, - OrderType: order.Limit, - Price: 1, - Amount: 1, - ClientID: "meowOrder", + Side: order.Buy, + Type: order.Limit, + Price: 1, + Amount: 1, + ClientID: "meowOrder", } response, err := b.SubmitOrder(orderSubmission) if areTestAPIKeysSet() && (err != nil || !response.IsOrderPlaced) { @@ -541,10 +543,10 @@ func TestCancelExchangeOrder(t *testing.T) { currencyPair := currency.NewPair(currency.LTC, currency.BTC) var orderCancellation = &order.Cancel{ - OrderID: "123456789012345678901234567890123456", + ID: "123456789012345678901234567890123456", WalletAddress: core.BitcoinDonationAddress, AccountID: "1", - CurrencyPair: currencyPair, + Pair: currencyPair, } err := b.CancelOrder(orderCancellation) @@ -563,10 +565,10 @@ func TestCancelAllExchangeOrders(t *testing.T) { currencyPair := currency.NewPair(currency.LTC, currency.BTC) var orderCancellation = &order.Cancel{ - OrderID: "123456789012345678901234567890123456", + ID: "123456789012345678901234567890123456", WalletAddress: core.BitcoinDonationAddress, AccountID: "1", - CurrencyPair: currencyPair, + Pair: currencyPair, } resp, err := b.CancelAllOrders(orderCancellation) @@ -601,7 +603,7 @@ func TestModifyOrder(t *testing.T) { if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - _, err := b.ModifyOrder(&order.Modify{OrderID: "1337"}) + _, err := b.ModifyOrder(&order.Modify{ID: "1337"}) if err == nil { t.Error("ModifyOrder() error") } @@ -686,9 +688,8 @@ func TestWsAuth(t *testing.T) { if err != nil { t.Fatal(err) } - b.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride() - b.Websocket.TrafficAlert = sharedtestvalues.GetWebsocketStructChannelOverride() - go b.wsHandleIncomingData() + + go b.wsReadData() err = b.websocketSendAuth() if err != nil { t.Fatal(err) @@ -704,3 +705,229 @@ func TestWsAuth(t *testing.T) { } timer.Stop() } + +func TestWsPositionUpdate(t *testing.T) { + pressXToJSON := []byte(`{"table":"position", + "action":"update", + "data":[{ + "account":2,"symbol":"ETHUSD","currency":"XBt", + "currentTimestamp":"2017-04-04T22:07:42.442Z", "currentQty":1,"markPrice":1136.88,"markValue":-87960, + "riskValue":87960,"homeNotional":0.0008796,"posState":"Liquidation","maintMargin":263, + "unrealisedGrossPnl":-677,"unrealisedPnl":-677,"unrealisedPnlPcnt":-0.0078,"unrealisedRoePcnt":-0.7756, + "simpleQty":0.001,"liquidationPrice":1140.1, "timestamp":"2017-04-04T22:07:45.442Z" + }]}`) + err := b.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsInsertExectuionUpdate(t *testing.T) { + pressXToJSON := []byte(`{"table":"execution", + "action":"insert", + "data":[{ + "execID":"0193e879-cb6f-2891-d099-2c4eb40fee21", + "orderID":"00000000-0000-0000-0000-000000000000","clOrdID":"","clOrdLinkID":"","account":2,"symbol":"ETHUSD", + "side":"Sell","lastQty":1,"lastPx":1134.37,"underlyingLastPx":null,"lastMkt":"XBME", + "lastLiquidityInd":"RemovedLiquidity", "simpleOrderQty":null,"orderQty":1,"price":1134.37,"displayQty":null, + "stopPx":null,"pegOffsetValue":null,"pegPriceType":"","currency":"USD","settlCurrency":"XBt", + "execType":"Trade","ordType":"Limit","timeInForce":"ImmediateOrCancel","execInst":"", + "contingencyType":"","exDestination":"XBME","ordStatus":"Filled","triggered":"","workingIndicator":false, + "ordRejReason":"","simpleLeavesQty":0,"leavesQty":0,"simpleCumQty":0.001,"cumQty":1,"avgPx":1134.37, + "commission":0.00075,"tradePublishIndicator":"DoNotPublishTrade","multiLegReportingType":"SingleSecurity", + "text":"Liquidation","trdMatchID":"7f4ab7f6-0006-3234-76f4-ae1385aad00f","execCost":88155,"execComm":66, + "homeNotional":-0.00088155,"foreignNotional":1,"transactTime":"2017-04-04T22:07:46.035Z", + "timestamp":"2017-04-04T22:07:46.035Z" + }]}`) + err := b.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWSConnectionHandling(t *testing.T) { + pressXToJSON := []byte(`{"info":"Welcome to the BitMEX Realtime API.","version":"1.1.0", + "timestamp":"2015-01-18T10:14:06.802Z","docs":"https://www.bitmex.com/app/wsAPI","heartbeatEnabled":false}`) + err := b.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWSSubscriptionHandling(t *testing.T) { + pressXToJSON := []byte(`{"success":true,"subscribe":"trade:ETHUSD", + "request":{"op":"subscribe","args":["trade:ETHUSD","instrument:ETHUSD"]}}`) + err := b.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWSPositionUpdateHandling(t *testing.T) { + pressXToJSON := []byte(`{"table":"position", + "action":"update", + "data":[{ + "account":2,"symbol":"ETHUSD","currency":"XBt","currentQty":1, + "markPrice":1136.88,"posState":"Liquidated","simpleQty":0.001,"liquidationPrice":1140.1,"bankruptPrice":1134.37, + "timestamp":"2017-04-04T22:07:46.019Z" + }]}`) + err := b.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } + pressXToJSON = []byte(`{"table":"position", + "action":"update", + "data":[{ + "account":2,"symbol":"ETHUSD","currency":"XBt", + "deleveragePercentile":null,"rebalancedPnl":1003,"prevRealisedPnl":-1003,"execSellQty":1, + "execSellCost":88155,"execQty":0,"execCost":872,"execComm":131,"currentTimestamp":"2017-04-04T22:07:46.140Z", + "currentQty":0,"currentCost":872,"currentComm":131,"realisedCost":872,"unrealisedCost":0,"grossExecCost":0, + "isOpen":false,"markPrice":null,"markValue":0,"riskValue":0,"homeNotional":0,"foreignNotional":0,"posState":"", + "posCost":0,"posCost2":0,"posInit":0,"posComm":0,"posMargin":0,"posMaint":0,"maintMargin":0, + "realisedGrossPnl":-872,"realisedPnl":-1003,"unrealisedGrossPnl":0,"unrealisedPnl":0, + "unrealisedPnlPcnt":0,"unrealisedRoePcnt":0,"simpleQty":0,"simpleCost":0,"simpleValue":0,"avgCostPrice":null, + "avgEntryPrice":null,"breakEvenPrice":null,"marginCallPrice":null,"liquidationPrice":null,"bankruptPrice":null, + "timestamp":"2017-04-04T22:07:46.140Z" + }]}`) + err = b.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWSOrderbookHandling(t *testing.T) { + b.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride() + pressXToJSON := []byte(`{ + "table":"orderBookL2_25", + "keys":["symbol","id","side"], + "types":{"id":"long","price":"float","side":"symbol","size":"long","symbol":"symbol"}, + "foreignKeys":{"side":"side","symbol":"instrument"}, + "attributes":{"id":"sorted","symbol":"grouped"}, + "action":"partial", + "data":[ + {"symbol":"ETHUSD","id":17999992000,"side":"Sell","size":100,"price":80}, + {"symbol":"ETHUSD","id":17999993000,"side":"Sell","size":20,"price":70}, + {"symbol":"ETHUSD","id":17999994000,"side":"Sell","size":10,"price":60}, + {"symbol":"ETHUSD","id":17999995000,"side":"Buy","size":10,"price":50}, + {"symbol":"ETHUSD","id":17999996000,"side":"Buy","size":20,"price":40}, + {"symbol":"ETHUSD","id":17999997000,"side":"Buy","size":100,"price":30} + ] + }`) + err := b.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } + + pressXToJSON = []byte(`{ + "table":"orderBookL2_25", + "action":"update", + "data":[ + {"symbol":"ETHUSD","id":17999995000,"side":"Buy","size":5} + ] + }`) + err = b.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } + + pressXToJSON = []byte(`{ + "table":"orderBookL2_25", + "action":"update", + "data":[ + ] + }`) + err = b.wsHandleData(pressXToJSON) + if err == nil { + t.Error("Expected error") + } + + pressXToJSON = []byte(`{ + "table":"orderBookL2_25", + "action":"delete", + "data":[ + {"symbol":"ETHUSD","id":17999995000,"side":"Buy"} + ] + }`) + err = b.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } + + pressXToJSON = []byte(`{ + "table":"orderBookL2_25", + "action":"delete", + "data":[ + {"symbol":"ETHUSD","id":17999995000,"side":"Buy"} + ] + }`) + err = b.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWSDeleveragePositionUpdateHandling(t *testing.T) { + b.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride() + pressXToJSON := []byte(`{"table":"position", + "action":"update", + "data":[{ + "account":2,"symbol":"ETHUSD","currency":"XBt","currentQty":2000, + "markPrice":1160.72,"posState":"Deleverage","simpleQty":1.746,"liquidationPrice":1140.1, + "timestamp":"2017-04-04T22:16:38.460Z" + }]}`) + err := b.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } + + pressXToJSON = []byte(`{"table":"position", + "action":"update", + "data":[{ + "account":2,"symbol":"ETHUSD","currency":"XBt", + "deleveragePercentile":null,"rebalancedPnl":-2171150,"prevRealisedPnl":2172153,"execSellQty":2001, + "execSellCost":172394155,"execQty":0,"execCost":-2259128,"execComm":87978, + "currentTimestamp":"2017-04-04T22:16:38.547Z","currentQty":0,"currentCost":-2259128, + "currentComm":87978,"realisedCost":-2259128,"unrealisedCost":0,"grossExecCost":0,"isOpen":false, + "markPrice":null,"markValue":0,"riskValue":0,"homeNotional":0,"foreignNotional":0,"posState":"","posCost":0, + "posCost2":0,"posInit":0,"posComm":0,"posMargin":0,"posMaint":0,"maintMargin":0,"realisedGrossPnl":2259128, + "realisedPnl":2171150,"unrealisedGrossPnl":0,"unrealisedPnl":0,"unrealisedPnlPcnt":0,"unrealisedRoePcnt":0, + "simpleQty":0,"simpleCost":0,"simpleValue":0,"simplePnl":0,"simplePnlPcnt":0,"avgCostPrice":null, + "avgEntryPrice":null,"breakEvenPrice":null,"marginCallPrice":null,"liquidationPrice":null,"bankruptPrice":null, + "timestamp":"2017-04-04T22:16:38.547Z" + }]}`) + err = b.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWSDeleverageExecutionInsertHandling(t *testing.T) { + pressXToJSON := []byte(`{"table":"execution", + "action":"insert", + "data":[{ + "execID":"20ad1ff4-c110-a4f2-dd31-f94eaa0701fd", + "orderID":"00000000-0000-0000-0000-000000000000","clOrdID":"","clOrdLinkID":"","account":2,"symbol":"ETHUSD", + "side":"Sell","lastQty":2000,"lastPx":1160.72,"underlyingLastPx":null,"lastMkt":"XBME", + "lastLiquidityInd":"AddedLiquidity","simpleOrderQty":null,"orderQty":2000,"price":1160.72,"displayQty":null, + "stopPx":null,"pegOffsetValue":null,"pegPriceType":"","currency":"USD","settlCurrency":"XBt","execType":"Trade", + "ordType":"Limit","timeInForce":"GoodTillCancel","execInst":"","contingencyType":"","exDestination":"XBME", + "ordStatus":"Filled","triggered":"","workingIndicator":false,"ordRejReason":"", + "simpleLeavesQty":0,"leavesQty":0,"simpleCumQty":1.746,"cumQty":2000,"avgPx":1160.72,"commission":-0.00025, + "tradePublishIndicator":"PublishTrade","multiLegReportingType":"SingleSecurity","text":"Deleverage", + "trdMatchID":"1e849b8a-7e88-3c67-a93f-cc654d40e8ba","execCost":172306000,"execComm":-43077, + "homeNotional":-1.72306,"foreignNotional":2000,"transactTime":"2017-04-04T22:16:38.472Z", + "timestamp":"2017-04-04T22:16:38.472Z" + }]}`) + err := b.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsTrades(t *testing.T) { + pressXToJSON := []byte(`{"table":"trade","action":"insert","data":[{"timestamp":"2020-02-17T01:35:36.442Z","symbol":"ETHUSD","side":"Sell","size":100,"price":258.3,"tickDirection":"MinusTick","trdMatchID":"c427f7a0-6b26-1e10-5c4e-1bd74daf2a73","grossValue":2583000,"homeNotional":0.9904912836767037,"foreignNotional":255.84389857369254},{"timestamp":"2020-02-17T01:35:36.442Z","symbol":"ETHUSD","side":"Sell","size":100,"price":258.3,"tickDirection":"ZeroMinusTick","trdMatchID":"95eb9155-b58c-70e9-44b7-34efe50302e0","grossValue":2583000,"homeNotional":0.9904912836767037,"foreignNotional":255.84389857369254},{"timestamp":"2020-02-17T01:35:36.442Z","symbol":"ETHUSD","side":"Sell","size":100,"price":258.3,"tickDirection":"ZeroMinusTick","trdMatchID":"e607c187-f25c-86bc-cb39-8afff7aaf2d9","grossValue":2583000,"homeNotional":0.9904912836767037,"foreignNotional":255.84389857369254},{"timestamp":"2020-02-17T01:35:36.442Z","symbol":"ETHUSD","side":"Sell","size":17,"price":258.3,"tickDirection":"ZeroMinusTick","trdMatchID":"0f076814-a57d-9a59-8063-ad6b823a80ac","grossValue":439110,"homeNotional":0.1683835182250396,"foreignNotional":43.49346275752773},{"timestamp":"2020-02-17T01:35:36.442Z","symbol":"ETHUSD","side":"Sell","size":100,"price":258.25,"tickDirection":"MinusTick","trdMatchID":"f4ef3dfd-51c4-538f-37c1-e5071ba1c75d","grossValue":2582500,"homeNotional":0.9904912836767037,"foreignNotional":255.79437400950872},{"timestamp":"2020-02-17T01:35:36.442Z","symbol":"ETHUSD","side":"Sell","size":100,"price":258.25,"tickDirection":"ZeroMinusTick","trdMatchID":"81ef136b-8f4a-b1cf-78a8-fffbfa89bf40","grossValue":2582500,"homeNotional":0.9904912836767037,"foreignNotional":255.79437400950872},{"timestamp":"2020-02-17T01:35:36.442Z","symbol":"ETHUSD","side":"Sell","size":100,"price":258.25,"tickDirection":"ZeroMinusTick","trdMatchID":"65a87e8c-7563-34a4-d040-94e8513c5401","grossValue":2582500,"homeNotional":0.9904912836767037,"foreignNotional":255.79437400950872},{"timestamp":"2020-02-17T01:35:36.442Z","symbol":"ETHUSD","side":"Sell","size":15,"price":258.25,"tickDirection":"ZeroMinusTick","trdMatchID":"1d11a74e-a157-3f33-036d-35a101fba50b","grossValue":387375,"homeNotional":0.14857369255150554,"foreignNotional":38.369156101426306},{"timestamp":"2020-02-17T01:35:36.442Z","symbol":"ETHUSD","side":"Sell","size":1,"price":258.25,"tickDirection":"ZeroMinusTick","trdMatchID":"40d49df1-f018-f66f-4ca5-31d4997641d7","grossValue":25825,"homeNotional":0.009904912836767036,"foreignNotional":2.5579437400950873},{"timestamp":"2020-02-17T01:35:36.442Z","symbol":"ETHUSD","side":"Sell","size":100,"price":258.2,"tickDirection":"MinusTick","trdMatchID":"36135b51-73e5-c007-362b-a55be5830c6b","grossValue":2582000,"homeNotional":0.9904912836767037,"foreignNotional":255.7448494453249},{"timestamp":"2020-02-17T01:35:36.442Z","symbol":"ETHUSD","side":"Sell","size":100,"price":258.2,"tickDirection":"ZeroMinusTick","trdMatchID":"6ee19edb-99aa-3030-ba63-933ffb347ade","grossValue":2582000,"homeNotional":0.9904912836767037,"foreignNotional":255.7448494453249},{"timestamp":"2020-02-17T01:35:36.442Z","symbol":"ETHUSD","side":"Sell","size":100,"price":258.2,"tickDirection":"ZeroMinusTick","trdMatchID":"d44be603-cdb8-d676-e3e2-f91fb12b2a70","grossValue":2582000,"homeNotional":0.9904912836767037,"foreignNotional":255.7448494453249},{"timestamp":"2020-02-17T01:35:36.442Z","symbol":"ETHUSD","side":"Sell","size":5,"price":258.2,"tickDirection":"ZeroMinusTick","trdMatchID":"a14b43b3-50b4-c075-c54d-dfb0165de33d","grossValue":129100,"homeNotional":0.04952456418383518,"foreignNotional":12.787242472266245},{"timestamp":"2020-02-17T01:35:36.442Z","symbol":"ETHUSD","side":"Sell","size":8,"price":258.2,"tickDirection":"ZeroMinusTick","trdMatchID":"3c30e175-5194-320c-8f8c-01636c2f4a32","grossValue":206560,"homeNotional":0.07923930269413629,"foreignNotional":20.45958795562599},{"timestamp":"2020-02-17T01:35:36.442Z","symbol":"ETHUSD","side":"Sell","size":50,"price":258.2,"tickDirection":"ZeroMinusTick","trdMatchID":"5b803378-760b-4919-21fc-bfb275d39ace","grossValue":1291000,"homeNotional":0.49524564183835185,"foreignNotional":127.87242472266244},{"timestamp":"2020-02-17T01:35:36.442Z","symbol":"ETHUSD","side":"Sell","size":244,"price":258.2,"tickDirection":"ZeroMinusTick","trdMatchID":"cf57fec1-c444-b9e5-5e2d-4fb643f4fdb7","grossValue":6300080,"homeNotional":2.416798732171157,"foreignNotional":624.0174326465927}]}`) + err := b.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} diff --git a/exchanges/bitmex/bitmex_types.go b/exchanges/bitmex/bitmex_types.go index 9445045b..4bcb98e9 100644 --- a/exchanges/bitmex/bitmex_types.go +++ b/exchanges/bitmex/bitmex_types.go @@ -62,62 +62,62 @@ type ConnectedUsers struct { // Execution Raw Order and Balance Data type Execution struct { - Account int64 `json:"account"` - AvgPx float64 `json:"avgPx"` - ClOrdID string `json:"clOrdID"` - ClOrdLinkID string `json:"clOrdLinkID"` - Commission float64 `json:"commission"` - ContingencyType string `json:"contingencyType"` - CumQty int64 `json:"cumQty"` - Currency string `json:"currency"` - DisplayQty int64 `json:"displayQty"` - ExDestination string `json:"exDestination"` - ExecComm int64 `json:"execComm"` - ExecCost int64 `json:"execCost"` - ExecID string `json:"execID"` - ExecInst string `json:"execInst"` - ExecType string `json:"execType"` - ForeignNotional float64 `json:"foreignNotional"` - HomeNotional float64 `json:"homeNotional"` - LastLiquidityInd string `json:"lastLiquidityInd"` - LastMkt string `json:"lastMkt"` - LastPx float64 `json:"lastPx"` - LastQty int64 `json:"lastQty"` - LeavesQty int64 `json:"leavesQty"` - MultiLegReportingType string `json:"multiLegReportingType"` - OrdRejReason string `json:"ordRejReason"` - OrdStatus string `json:"ordStatus"` - OrdType string `json:"ordType"` - OrderID string `json:"orderID"` - OrderQty int64 `json:"orderQty"` - PegOffsetValue float64 `json:"pegOffsetValue"` - PegPriceType string `json:"pegPriceType"` - Price float64 `json:"price"` - SettlCurrency string `json:"settlCurrency"` - Side string `json:"side"` - SimpleCumQty float64 `json:"simpleCumQty"` - SimpleLeavesQty float64 `json:"simpleLeavesQty"` - SimpleOrderQty float64 `json:"simpleOrderQty"` - StopPx float64 `json:"stopPx"` - Symbol string `json:"symbol"` - Text string `json:"text"` - TimeInForce string `json:"timeInForce"` - Timestamp string `json:"timestamp"` - TradePublishIndicator string `json:"tradePublishIndicator"` - TransactTime string `json:"transactTime"` - TrdMatchID string `json:"trdMatchID"` - Triggered string `json:"triggered"` - UnderlyingLastPx float64 `json:"underlyingLastPx"` - WorkingIndicator bool `json:"workingIndicator"` + Account int64 `json:"account"` + AvgPx float64 `json:"avgPx"` + ClOrdID string `json:"clOrdID"` + ClOrdLinkID string `json:"clOrdLinkID"` + Commission float64 `json:"commission"` + ContingencyType string `json:"contingencyType"` + CumQty int64 `json:"cumQty"` + Currency string `json:"currency"` + DisplayQuantity int64 `json:"displayQty"` + ExDestination string `json:"exDestination"` + ExecComm int64 `json:"execComm"` + ExecCost int64 `json:"execCost"` + ExecID string `json:"execID"` + ExecInst string `json:"execInst"` + ExecType string `json:"execType"` + ForeignNotional float64 `json:"foreignNotional"` + HomeNotional float64 `json:"homeNotional"` + LastLiquidityInd string `json:"lastLiquidityInd"` + LastMkt string `json:"lastMkt"` + LastPx float64 `json:"lastPx"` + LastQty int64 `json:"lastQty"` + LeavesQty int64 `json:"leavesQty"` + MultiLegReportingType string `json:"multiLegReportingType"` + OrdRejReason string `json:"ordRejReason"` + OrdStatus string `json:"ordStatus"` + OrdType string `json:"ordType"` + OrderID string `json:"orderID"` + OrderQty int64 `json:"orderQty"` + PegOffsetValue float64 `json:"pegOffsetValue"` + PegPriceType string `json:"pegPriceType"` + Price float64 `json:"price"` + SettlCurrency string `json:"settlCurrency"` + Side string `json:"side"` + SimpleCumQty float64 `json:"simpleCumQty"` + SimpleLeavesQty float64 `json:"simpleLeavesQty"` + SimpleOrderQty float64 `json:"simpleOrderQty"` + StopPx float64 `json:"stopPx"` + Symbol string `json:"symbol"` + Text string `json:"text"` + TimeInForce string `json:"timeInForce"` + Timestamp time.Time `json:"timestamp"` + TradePublishIndicator string `json:"tradePublishIndicator"` + TransactTime string `json:"transactTime"` + TrdMatchID string `json:"trdMatchID"` + Triggered string `json:"triggered"` + UnderlyingLastPx float64 `json:"underlyingLastPx"` + WorkingIndicator bool `json:"workingIndicator"` } // Funding Swap Funding History type Funding struct { - FundingInterval string `json:"fundingInterval"` - FundingRate float64 `json:"fundingRate"` - FundingRateDaily float64 `json:"fundingRateDaily"` - Symbol string `json:"symbol"` - Timestamp string `json:"timestamp"` + FundingInterval string `json:"fundingInterval"` + FundingRate float64 `json:"fundingRate"` + FundingRateDaily float64 `json:"fundingRateDaily"` + Symbol string `json:"symbol"` + Timestamp time.Time `json:"timestamp"` } // Instrument Tradeable Contracts, Indices, and History @@ -129,7 +129,7 @@ type Instrument struct { BuyLeg string `json:"buyLeg"` CalcInterval string `json:"calcInterval"` Capped bool `json:"capped"` - ClosingTimestamp string `json:"closingTimestamp"` + ClosingTimestamp time.Time `json:"closingTimestamp"` Deleverage bool `json:"deleverage"` Expiry string `json:"expiry"` FairBasis float64 `json:"fairBasis"` @@ -142,7 +142,7 @@ type Instrument struct { FundingPremiumSymbol string `json:"fundingPremiumSymbol"` FundingQuoteSymbol string `json:"fundingQuoteSymbol"` FundingRate float64 `json:"fundingRate"` - FundingTimestamp string `json:"fundingTimestamp"` + FundingTimestamp time.Time `json:"fundingTimestamp"` HasLiquidity bool `json:"hasLiquidity"` HighPrice float64 `json:"highPrice"` ImpactAskPrice float64 `json:"impactAskPrice"` @@ -176,7 +176,7 @@ type Instrument struct { Multiplier int64 `json:"multiplier"` OpenInterest int64 `json:"openInterest"` OpenValue int64 `json:"openValue"` - OpeningTimestamp string `json:"openingTimestamp"` + OpeningTimestamp time.Time `json:"openingTimestamp"` OptionMultiplier float64 `json:"optionMultiplier"` OptionStrikePcnt float64 `json:"optionStrikePcnt"` OptionStrikePrice float64 `json:"optionStrikePrice"` @@ -192,7 +192,7 @@ type Instrument struct { QuoteCurrency string `json:"quoteCurrency"` QuoteToSettleMultiplier int64 `json:"quoteToSettleMultiplier"` RebalanceInterval string `json:"rebalanceInterval"` - RebalanceTimestamp string `json:"rebalanceTimestamp"` + RebalanceTimestamp time.Time `json:"rebalanceTimestamp"` Reference string `json:"reference"` ReferenceSymbol string `json:"referenceSymbol"` RelistInterval string `json:"relistInterval"` @@ -233,20 +233,20 @@ type InstrumentInterval struct { // IndexComposite index composite type IndexComposite struct { - IndexSymbol string `json:"indexSymbol"` - LastPrice float64 `json:"lastPrice"` - Logged string `json:"logged"` - Reference string `json:"reference"` - Symbol string `json:"symbol"` - Timestamp string `json:"timestamp"` - Weight float64 `json:"weight"` + IndexSymbol string `json:"indexSymbol"` + LastPrice float64 `json:"lastPrice"` + Logged string `json:"logged"` + Reference string `json:"reference"` + Symbol string `json:"symbol"` + Timestamp time.Time `json:"timestamp"` + Weight float64 `json:"weight"` } // Insurance Insurance Fund Data type Insurance struct { - Currency string `json:"currency"` - Timestamp string `json:"timestamp"` - WalletBalance int64 `json:"walletBalance"` + Currency string `json:"currency"` + Timestamp time.Time `json:"timestamp"` + WalletBalance int64 `json:"walletBalance"` } // Leaderboard Information on Top Users @@ -286,39 +286,39 @@ type Notification struct { // Order Placement, Cancellation, Amending, and History type Order struct { - Account int64 `json:"account"` - AvgPx float64 `json:"avgPx"` - ClOrdID string `json:"clOrdID"` - ClOrdLinkID string `json:"clOrdLinkID"` - ContingencyType string `json:"contingencyType"` - CumQty int64 `json:"cumQty"` - Currency string `json:"currency"` - DisplayQty int64 `json:"displayQty"` - ExDestination string `json:"exDestination"` - ExecInst string `json:"execInst"` - LeavesQty int64 `json:"leavesQty"` - MultiLegReportingType string `json:"multiLegReportingType"` - OrdRejReason string `json:"ordRejReason"` - OrdStatus string `json:"ordStatus"` - OrdType int64 `json:"ordType,string"` - OrderID string `json:"orderID"` - OrderQty int64 `json:"orderQty"` - PegOffsetValue float64 `json:"pegOffsetValue"` - PegPriceType string `json:"pegPriceType"` - Price float64 `json:"price"` - SettlCurrency string `json:"settlCurrency"` - Side int64 `json:"side,string"` - SimpleCumQty float64 `json:"simpleCumQty"` - SimpleLeavesQty float64 `json:"simpleLeavesQty"` - SimpleOrderQty float64 `json:"simpleOrderQty"` - StopPx float64 `json:"stopPx"` - Symbol string `json:"symbol"` - Text string `json:"text"` - TimeInForce string `json:"timeInForce"` - Timestamp string `json:"timestamp"` - TransactTime string `json:"transactTime"` - Triggered string `json:"triggered"` - WorkingIndicator bool `json:"workingIndicator"` + Account int64 `json:"account"` + AvgPx float64 `json:"avgPx"` + ClOrdID string `json:"clOrdID"` + ClOrdLinkID string `json:"clOrdLinkID"` + ContingencyType string `json:"contingencyType"` + CumQty int64 `json:"cumQty"` + Currency string `json:"currency"` + DisplayQuantity int64 `json:"displayQty"` + ExDestination string `json:"exDestination"` + ExecInst string `json:"execInst"` + LeavesQty int64 `json:"leavesQty"` + MultiLegReportingType string `json:"multiLegReportingType"` + OrdRejReason string `json:"ordRejReason"` + OrdStatus string `json:"ordStatus"` + OrdType int64 `json:"ordType,string"` + OrderID string `json:"orderID"` + OrderQty int64 `json:"orderQty"` + PegOffsetValue float64 `json:"pegOffsetValue"` + PegPriceType string `json:"pegPriceType"` + Price float64 `json:"price"` + SettlCurrency string `json:"settlCurrency"` + Side int64 `json:"side,string"` + SimpleCumQty float64 `json:"simpleCumQty"` + SimpleLeavesQty float64 `json:"simpleLeavesQty"` + SimpleOrderQty float64 `json:"simpleOrderQty"` + StopPx float64 `json:"stopPx"` + Symbol string `json:"symbol"` + Text string `json:"text"` + TimeInForce string `json:"timeInForce"` + Timestamp time.Time `json:"timestamp"` + TransactTime string `json:"transactTime"` + Triggered string `json:"triggered"` + WorkingIndicator bool `json:"workingIndicator"` } // OrderBookL2 contains order book l2 @@ -332,120 +332,120 @@ type OrderBookL2 struct { // Position Summary of Open and Closed Positions type Position struct { - Account int64 `json:"account"` - AvgCostPrice float64 `json:"avgCostPrice"` - AvgEntryPrice float64 `json:"avgEntryPrice"` - BankruptPrice float64 `json:"bankruptPrice"` - BreakEvenPrice float64 `json:"breakEvenPrice"` - Commission float64 `json:"commission"` - CrossMargin bool `json:"crossMargin"` - Currency string `json:"currency"` - CurrentComm int64 `json:"currentComm"` - CurrentCost int64 `json:"currentCost"` - CurrentQty int64 `json:"currentQty"` - CurrentTimestamp string `json:"currentTimestamp"` - DeleveragePercentile float64 `json:"deleveragePercentile"` - ExecBuyCost int64 `json:"execBuyCost"` - ExecBuyQty int64 `json:"execBuyQty"` - ExecComm int64 `json:"execComm"` - ExecCost int64 `json:"execCost"` - ExecQty int64 `json:"execQty"` - ExecSellCost int64 `json:"execSellCost"` - ExecSellQty int64 `json:"execSellQty"` - ForeignNotional float64 `json:"foreignNotional"` - GrossExecCost int64 `json:"grossExecCost"` - GrossOpenCost int64 `json:"grossOpenCost"` - GrossOpenPremium int64 `json:"grossOpenPremium"` - HomeNotional float64 `json:"homeNotional"` - IndicativeTax int64 `json:"indicativeTax"` - IndicativeTaxRate float64 `json:"indicativeTaxRate"` - InitMargin int64 `json:"initMargin"` - InitMarginReq float64 `json:"initMarginReq"` - IsOpen bool `json:"isOpen"` - LastPrice float64 `json:"lastPrice"` - LastValue int64 `json:"lastValue"` - Leverage float64 `json:"leverage"` - LiquidationPrice float64 `json:"liquidationPrice"` - LongBankrupt int64 `json:"longBankrupt"` - MaintMargin int64 `json:"maintMargin"` - MaintMarginReq float64 `json:"maintMarginReq"` - MarginCallPrice float64 `json:"marginCallPrice"` - MarkPrice float64 `json:"markPrice"` - MarkValue int64 `json:"markValue"` - OpenOrderBuyCost int64 `json:"openOrderBuyCost"` - OpenOrderBuyPremium int64 `json:"openOrderBuyPremium"` - OpenOrderBuyQty int64 `json:"openOrderBuyQty"` - OpenOrderSellCost int64 `json:"openOrderSellCost"` - OpenOrderSellPremium int64 `json:"openOrderSellPremium"` - OpenOrderSellQty int64 `json:"openOrderSellQty"` - OpeningComm int64 `json:"openingComm"` - OpeningCost int64 `json:"openingCost"` - OpeningQty int64 `json:"openingQty"` - OpeningTimestamp string `json:"openingTimestamp"` - PosAllowance int64 `json:"posAllowance"` - PosComm int64 `json:"posComm"` - PosCost int64 `json:"posCost"` - PosCost2 int64 `json:"posCost2"` - PosCross int64 `json:"posCross"` - PosInit int64 `json:"posInit"` - PosLoss int64 `json:"posLoss"` - PosMaint int64 `json:"posMaint"` - PosMargin int64 `json:"posMargin"` - PosState string `json:"posState"` - PrevClosePrice float64 `json:"prevClosePrice"` - PrevRealisedPnl int64 `json:"prevRealisedPnl"` - PrevUnrealisedPnl int64 `json:"prevUnrealisedPnl"` - QuoteCurrency string `json:"quoteCurrency"` - RealisedCost int64 `json:"realisedCost"` - RealisedGrossPnl int64 `json:"realisedGrossPnl"` - RealisedPnl int64 `json:"realisedPnl"` - RealisedTax int64 `json:"realisedTax"` - RebalancedPnl int64 `json:"rebalancedPnl"` - RiskLimit int64 `json:"riskLimit"` - RiskValue int64 `json:"riskValue"` - SessionMargin int64 `json:"sessionMargin"` - ShortBankrupt int64 `json:"shortBankrupt"` - SimpleCost float64 `json:"simpleCost"` - SimplePnl float64 `json:"simplePnl"` - SimplePnlPcnt float64 `json:"simplePnlPcnt"` - SimpleQty float64 `json:"simpleQty"` - SimpleValue float64 `json:"simpleValue"` - Symbol string `json:"symbol"` - TargetExcessMargin int64 `json:"targetExcessMargin"` - TaxBase int64 `json:"taxBase"` - TaxableMargin int64 `json:"taxableMargin"` - Timestamp string `json:"timestamp"` - Underlying string `json:"underlying"` - UnrealisedCost int64 `json:"unrealisedCost"` - UnrealisedGrossPnl int64 `json:"unrealisedGrossPnl"` - UnrealisedPnl int64 `json:"unrealisedPnl"` - UnrealisedPnlPcnt float64 `json:"unrealisedPnlPcnt"` - UnrealisedRoePcnt float64 `json:"unrealisedRoePcnt"` - UnrealisedTax int64 `json:"unrealisedTax"` - VarMargin int64 `json:"varMargin"` + Account int64 `json:"account"` + AvgCostPrice float64 `json:"avgCostPrice"` + AvgEntryPrice float64 `json:"avgEntryPrice"` + BankruptPrice float64 `json:"bankruptPrice"` + BreakEvenPrice float64 `json:"breakEvenPrice"` + Commission float64 `json:"commission"` + CrossMargin bool `json:"crossMargin"` + Currency string `json:"currency"` + CurrentComm int64 `json:"currentComm"` + CurrentCost int64 `json:"currentCost"` + CurrentQty int64 `json:"currentQty"` + CurrentTimestamp time.Time `json:"currentTimestamp"` + DeleveragePercentile float64 `json:"deleveragePercentile"` + ExecBuyCost int64 `json:"execBuyCost"` + ExecBuyQty int64 `json:"execBuyQty"` + ExecComm int64 `json:"execComm"` + ExecCost int64 `json:"execCost"` + ExecQty int64 `json:"execQty"` + ExecSellCost int64 `json:"execSellCost"` + ExecSellQty int64 `json:"execSellQty"` + ForeignNotional float64 `json:"foreignNotional"` + GrossExecCost int64 `json:"grossExecCost"` + GrossOpenCost int64 `json:"grossOpenCost"` + GrossOpenPremium int64 `json:"grossOpenPremium"` + HomeNotional float64 `json:"homeNotional"` + IndicativeTax int64 `json:"indicativeTax"` + IndicativeTaxRate float64 `json:"indicativeTaxRate"` + InitMargin int64 `json:"initMargin"` + InitMarginReq float64 `json:"initMarginReq"` + IsOpen bool `json:"isOpen"` + LastPrice float64 `json:"lastPrice"` + LastValue int64 `json:"lastValue"` + Leverage float64 `json:"leverage"` + LiquidationPrice float64 `json:"liquidationPrice"` + LongBankrupt int64 `json:"longBankrupt"` + MaintMargin int64 `json:"maintMargin"` + MaintMarginReq float64 `json:"maintMarginReq"` + MarginCallPrice float64 `json:"marginCallPrice"` + MarkPrice float64 `json:"markPrice"` + MarkValue int64 `json:"markValue"` + OpenOrderBuyCost int64 `json:"openOrderBuyCost"` + OpenOrderBuyPremium int64 `json:"openOrderBuyPremium"` + OpenOrderBuyQty int64 `json:"openOrderBuyQty"` + OpenOrderSellCost int64 `json:"openOrderSellCost"` + OpenOrderSellPremium int64 `json:"openOrderSellPremium"` + OpenOrderSellQty int64 `json:"openOrderSellQty"` + OpeningComm int64 `json:"openingComm"` + OpeningCost int64 `json:"openingCost"` + OpeningQty int64 `json:"openingQty"` + OpeningTimestamp time.Time `json:"openingTimestamp"` + PosAllowance int64 `json:"posAllowance"` + PosComm int64 `json:"posComm"` + PosCost int64 `json:"posCost"` + PosCost2 int64 `json:"posCost2"` + PosCross int64 `json:"posCross"` + PosInit int64 `json:"posInit"` + PosLoss int64 `json:"posLoss"` + PosMaint int64 `json:"posMaint"` + PosMargin int64 `json:"posMargin"` + PosState string `json:"posState"` + PrevClosePrice float64 `json:"prevClosePrice"` + PrevRealisedPnl int64 `json:"prevRealisedPnl"` + PrevUnrealisedPnl int64 `json:"prevUnrealisedPnl"` + QuoteCurrency string `json:"quoteCurrency"` + RealisedCost int64 `json:"realisedCost"` + RealisedGrossPnl int64 `json:"realisedGrossPnl"` + RealisedPnl int64 `json:"realisedPnl"` + RealisedTax int64 `json:"realisedTax"` + RebalancedPnl int64 `json:"rebalancedPnl"` + RiskLimit int64 `json:"riskLimit"` + RiskValue int64 `json:"riskValue"` + SessionMargin int64 `json:"sessionMargin"` + ShortBankrupt int64 `json:"shortBankrupt"` + SimpleCost float64 `json:"simpleCost"` + SimplePnl float64 `json:"simplePnl"` + SimplePnlPcnt float64 `json:"simplePnlPcnt"` + SimpleQty float64 `json:"simpleQty"` + SimpleValue float64 `json:"simpleValue"` + Symbol string `json:"symbol"` + TargetExcessMargin int64 `json:"targetExcessMargin"` + TaxBase int64 `json:"taxBase"` + TaxableMargin int64 `json:"taxableMargin"` + Timestamp time.Time `json:"timestamp"` + Underlying string `json:"underlying"` + UnrealisedCost int64 `json:"unrealisedCost"` + UnrealisedGrossPnl int64 `json:"unrealisedGrossPnl"` + UnrealisedPnl int64 `json:"unrealisedPnl"` + UnrealisedPnlPcnt float64 `json:"unrealisedPnlPcnt"` + UnrealisedRoePcnt float64 `json:"unrealisedRoePcnt"` + UnrealisedTax int64 `json:"unrealisedTax"` + VarMargin int64 `json:"varMargin"` } // Quote Best Bid/Offer Snapshots & Historical Bins type Quote struct { - AskPrice float64 `json:"askPrice"` - AskSize int64 `json:"askSize"` - BidPrice float64 `json:"bidPrice"` - BidSize int64 `json:"bidSize"` - Symbol string `json:"symbol"` - Timestamp string `json:"timestamp"` + AskPrice float64 `json:"askPrice"` + AskSize int64 `json:"askSize"` + BidPrice float64 `json:"bidPrice"` + BidSize int64 `json:"bidSize"` + Symbol string `json:"symbol"` + Timestamp time.Time `json:"timestamp"` } // Settlement Historical Settlement Data type Settlement struct { - Bankrupt int64 `json:"bankrupt"` - OptionStrikePrice float64 `json:"optionStrikePrice"` - OptionUnderlyingPrice float64 `json:"optionUnderlyingPrice"` - SettledPrice float64 `json:"settledPrice"` - SettlementType string `json:"settlementType"` - Symbol string `json:"symbol"` - TaxBase int64 `json:"taxBase"` - TaxRate float64 `json:"taxRate"` - Timestamp string `json:"timestamp"` + Bankrupt int64 `json:"bankrupt"` + OptionStrikePrice float64 `json:"optionStrikePrice"` + OptionUnderlyingPrice float64 `json:"optionUnderlyingPrice"` + SettledPrice float64 `json:"settledPrice"` + SettlementType string `json:"settlementType"` + Symbol string `json:"symbol"` + TaxBase int64 `json:"taxBase"` + TaxRate float64 `json:"taxRate"` + Timestamp time.Time `json:"timestamp"` } // Stats Exchange Statistics @@ -479,16 +479,16 @@ type StatsUSD struct { // Trade Individual & Bucketed Trades type Trade struct { - ForeignNotional float64 `json:"foreignNotional"` - GrossValue int64 `json:"grossValue"` - HomeNotional float64 `json:"homeNotional"` - Price float64 `json:"price"` - Side string `json:"side"` - Size int64 `json:"size"` - Symbol string `json:"symbol"` - TickDirection string `json:"tickDirection"` - Timestamp string `json:"timestamp"` - TrdMatchID string `json:"trdMatchID"` + ForeignNotional float64 `json:"foreignNotional"` + GrossValue int64 `json:"grossValue"` + HomeNotional float64 `json:"homeNotional"` + Price float64 `json:"price"` + Side string `json:"side"` + Size int64 `json:"size"` + Symbol string `json:"symbol"` + TickDirection string `json:"tickDirection"` + Timestamp time.Time `json:"timestamp"` + TrdMatchID string `json:"trdMatchID"` } // User Account Operations @@ -544,37 +544,37 @@ type UserPreferences struct { // AffiliateStatus affiliate Status details type AffiliateStatus struct { - Account int64 `json:"account"` - Currency string `json:"currency"` - ExecComm int64 `json:"execComm"` - ExecTurnover int64 `json:"execTurnover"` - PayoutPcnt float64 `json:"payoutPcnt"` - PendingPayout int64 `json:"pendingPayout"` - PrevComm int64 `json:"prevComm"` - PrevPayout int64 `json:"prevPayout"` - PrevTimestamp string `json:"prevTimestamp"` - PrevTurnover int64 `json:"prevTurnover"` - ReferrerAccount float64 `json:"referrerAccount"` - Timestamp string `json:"timestamp"` - TotalComm int64 `json:"totalComm"` - TotalReferrals int64 `json:"totalReferrals"` - TotalTurnover int64 `json:"totalTurnover"` + Account int64 `json:"account"` + Currency string `json:"currency"` + ExecComm int64 `json:"execComm"` + ExecTurnover int64 `json:"execTurnover"` + PayoutPcnt float64 `json:"payoutPcnt"` + PendingPayout int64 `json:"pendingPayout"` + PrevComm int64 `json:"prevComm"` + PrevPayout int64 `json:"prevPayout"` + PrevTimestamp time.Time `json:"prevTimestamp"` + PrevTurnover int64 `json:"prevTurnover"` + ReferrerAccount float64 `json:"referrerAccount"` + Timestamp time.Time `json:"timestamp"` + TotalComm int64 `json:"totalComm"` + TotalReferrals int64 `json:"totalReferrals"` + TotalTurnover int64 `json:"totalTurnover"` } // TransactionInfo Information type TransactionInfo struct { - Account int64 `json:"account"` - Address string `json:"address"` - Amount int64 `json:"amount"` - Currency string `json:"currency"` - Fee int64 `json:"fee"` - Text string `json:"text"` - Timestamp string `json:"timestamp"` - TransactID string `json:"transactID"` - TransactStatus string `json:"transactStatus"` - TransactTime string `json:"transactTime"` - TransactType string `json:"transactType"` - Tx string `json:"tx"` + Account int64 `json:"account"` + Address string `json:"address"` + Amount int64 `json:"amount"` + Currency string `json:"currency"` + Fee int64 `json:"fee"` + Text string `json:"text"` + Timestamp time.Time `json:"timestamp"` + TransactID string `json:"transactID"` + TransactStatus string `json:"transactStatus"` + TransactTime string `json:"transactTime"` + TransactType string `json:"transactType"` + Tx string `json:"tx"` } // UserCommission user commission @@ -595,47 +595,47 @@ type ConfirmEmail struct { // UserMargin margin information type UserMargin struct { - Account int64 `json:"account"` - Action string `json:"action"` - Amount int64 `json:"amount"` - AvailableMargin int64 `json:"availableMargin"` - Commission float64 `json:"commission"` - ConfirmedDebit int64 `json:"confirmedDebit"` - Currency string `json:"currency"` - ExcessMargin int64 `json:"excessMargin"` - ExcessMarginPcnt float64 `json:"excessMarginPcnt"` - GrossComm int64 `json:"grossComm"` - GrossExecCost int64 `json:"grossExecCost"` - GrossLastValue int64 `json:"grossLastValue"` - GrossMarkValue int64 `json:"grossMarkValue"` - GrossOpenCost int64 `json:"grossOpenCost"` - GrossOpenPremium int64 `json:"grossOpenPremium"` - IndicativeTax int64 `json:"indicativeTax"` - InitMargin int64 `json:"initMargin"` - MaintMargin int64 `json:"maintMargin"` - MarginBalance int64 `json:"marginBalance"` - MarginBalancePcnt float64 `json:"marginBalancePcnt"` - MarginLeverage float64 `json:"marginLeverage"` - MarginUsedPcnt float64 `json:"marginUsedPcnt"` - PendingCredit int64 `json:"pendingCredit"` - PendingDebit int64 `json:"pendingDebit"` - PrevRealisedPnl int64 `json:"prevRealisedPnl"` - PrevState string `json:"prevState"` - PrevUnrealisedPnl int64 `json:"prevUnrealisedPnl"` - RealisedPnl int64 `json:"realisedPnl"` - RiskLimit int64 `json:"riskLimit"` - RiskValue int64 `json:"riskValue"` - SessionMargin int64 `json:"sessionMargin"` - State string `json:"state"` - SyntheticMargin int64 `json:"syntheticMargin"` - TargetExcessMargin int64 `json:"targetExcessMargin"` - TaxableMargin int64 `json:"taxableMargin"` - Timestamp string `json:"timestamp"` - UnrealisedPnl int64 `json:"unrealisedPnl"` - UnrealisedProfit int64 `json:"unrealisedProfit"` - VarMargin int64 `json:"varMargin"` - WalletBalance int64 `json:"walletBalance"` - WithdrawableMargin int64 `json:"withdrawableMargin"` + Account int64 `json:"account"` + Action string `json:"action"` + Amount int64 `json:"amount"` + AvailableMargin int64 `json:"availableMargin"` + Commission float64 `json:"commission"` + ConfirmedDebit int64 `json:"confirmedDebit"` + Currency string `json:"currency"` + ExcessMargin int64 `json:"excessMargin"` + ExcessMarginPcnt float64 `json:"excessMarginPcnt"` + GrossComm int64 `json:"grossComm"` + GrossExecCost int64 `json:"grossExecCost"` + GrossLastValue int64 `json:"grossLastValue"` + GrossMarkValue int64 `json:"grossMarkValue"` + GrossOpenCost int64 `json:"grossOpenCost"` + GrossOpenPremium int64 `json:"grossOpenPremium"` + IndicativeTax int64 `json:"indicativeTax"` + InitMargin int64 `json:"initMargin"` + MaintMargin int64 `json:"maintMargin"` + MarginBalance int64 `json:"marginBalance"` + MarginBalancePcnt float64 `json:"marginBalancePcnt"` + MarginLeverage float64 `json:"marginLeverage"` + MarginUsedPcnt float64 `json:"marginUsedPcnt"` + PendingCredit int64 `json:"pendingCredit"` + PendingDebit int64 `json:"pendingDebit"` + PrevRealisedPnl int64 `json:"prevRealisedPnl"` + PrevState string `json:"prevState"` + PrevUnrealisedPnl int64 `json:"prevUnrealisedPnl"` + RealisedPnl int64 `json:"realisedPnl"` + RiskLimit int64 `json:"riskLimit"` + RiskValue int64 `json:"riskValue"` + SessionMargin int64 `json:"sessionMargin"` + State string `json:"state"` + SyntheticMargin int64 `json:"syntheticMargin"` + TargetExcessMargin int64 `json:"targetExcessMargin"` + TaxableMargin int64 `json:"taxableMargin"` + Timestamp time.Time `json:"timestamp"` + UnrealisedPnl int64 `json:"unrealisedPnl"` + UnrealisedProfit int64 `json:"unrealisedProfit"` + VarMargin int64 `json:"varMargin"` + WalletBalance int64 `json:"walletBalance"` + WithdrawableMargin int64 `json:"withdrawableMargin"` } // MinWithdrawalFee minimum withdrawal fee information @@ -647,31 +647,31 @@ type MinWithdrawalFee struct { // WalletInfo wallet information type WalletInfo struct { - Account int64 `json:"account"` - Addr string `json:"addr"` - Amount int64 `json:"amount"` - ConfirmedDebit int64 `json:"confirmedDebit"` - Currency string `json:"currency"` - DeltaAmount int64 `json:"deltaAmount"` - DeltaDeposited int64 `json:"deltaDeposited"` - DeltaTransferIn int64 `json:"deltaTransferIn"` - DeltaTransferOut int64 `json:"deltaTransferOut"` - DeltaWithdrawn int64 `json:"deltaWithdrawn"` - Deposited int64 `json:"deposited"` - PendingCredit int64 `json:"pendingCredit"` - PendingDebit int64 `json:"pendingDebit"` - PrevAmount int64 `json:"prevAmount"` - PrevDeposited int64 `json:"prevDeposited"` - PrevTimestamp string `json:"prevTimestamp"` - PrevTransferIn int64 `json:"prevTransferIn"` - PrevTransferOut int64 `json:"prevTransferOut"` - PrevWithdrawn int64 `json:"prevWithdrawn"` - Script string `json:"script"` - Timestamp string `json:"timestamp"` - TransferIn int64 `json:"transferIn"` - TransferOut int64 `json:"transferOut"` - WithdrawalLock []string `json:"withdrawalLock"` - Withdrawn int64 `json:"withdrawn"` + Account int64 `json:"account"` + Addr string `json:"addr"` + Amount int64 `json:"amount"` + ConfirmedDebit int64 `json:"confirmedDebit"` + Currency string `json:"currency"` + DeltaAmount int64 `json:"deltaAmount"` + DeltaDeposited int64 `json:"deltaDeposited"` + DeltaTransferIn int64 `json:"deltaTransferIn"` + DeltaTransferOut int64 `json:"deltaTransferOut"` + DeltaWithdrawn int64 `json:"deltaWithdrawn"` + Deposited int64 `json:"deposited"` + PendingCredit int64 `json:"pendingCredit"` + PendingDebit int64 `json:"pendingDebit"` + PrevAmount int64 `json:"prevAmount"` + PrevDeposited int64 `json:"prevDeposited"` + PrevTimestamp time.Time `json:"prevTimestamp"` + PrevTransferIn int64 `json:"prevTransferIn"` + PrevTransferOut int64 `json:"prevTransferOut"` + PrevWithdrawn int64 `json:"prevWithdrawn"` + Script string `json:"script"` + Timestamp time.Time `json:"timestamp"` + TransferIn int64 `json:"transferIn"` + TransferOut int64 `json:"transferOut"` + WithdrawalLock []string `json:"withdrawalLock"` + Withdrawn int64 `json:"withdrawn"` } // orderTypeMap holds order type info based on Bitmex data diff --git a/exchanges/bitmex/bitmex_websocket.go b/exchanges/bitmex/bitmex_websocket.go index d29b2d1c..26e0cead 100644 --- a/exchanges/bitmex/bitmex_websocket.go +++ b/exchanges/bitmex/bitmex_websocket.go @@ -33,6 +33,7 @@ const ( bitmexWSInsurance = "insurance" bitmexWSLiquidation = "liquidation" bitmexWSOrderbookL2 = "orderBookL2" + bitmexWSOrderbookL225 = "orderBookL2_25" bitmexWSOrderbookL10 = "orderBook10" bitmexWSPublicNotifications = "publicNotifications" bitmexWSQuote = "quote" @@ -93,7 +94,7 @@ func (b *Bitmex) WsConnect() error { welcomeResp.Limit.Remaining) } - go b.wsHandleIncomingData() + go b.wsReadData() b.GenerateDefaultSubscriptions() err = b.websocketSendAuth() if err != nil { @@ -103,8 +104,8 @@ func (b *Bitmex) WsConnect() error { return nil } -// wsHandleIncomingData services incoming data from the websocket connection -func (b *Bitmex) wsHandleIncomingData() { +// wsReadData receives and passes on websocket messages for processing +func (b *Bitmex) wsReadData() { b.Websocket.Wg.Add(1) defer func() { @@ -123,203 +124,348 @@ func (b *Bitmex) wsHandleIncomingData() { return } b.Websocket.TrafficAlert <- struct{}{} - - quickCapture := make(map[string]interface{}) - err = json.Unmarshal(resp.Raw, &quickCapture) + err = b.wsHandleData(resp.Raw) if err != nil { b.Websocket.DataHandler <- err - continue - } - - var respError WebsocketErrorResponse - if _, ok := quickCapture["status"]; ok { - err = json.Unmarshal(resp.Raw, &respError) - if err != nil { - b.Websocket.DataHandler <- err - continue - } - b.Websocket.DataHandler <- errors.New(respError.Error) - continue - } - - if _, ok := quickCapture["success"]; ok { - var decodedResp WebsocketSubscribeResp - err := json.Unmarshal(resp.Raw, &decodedResp) - if err != nil { - b.Websocket.DataHandler <- err - continue - } - - if decodedResp.Success { - b.Websocket.DataHandler <- decodedResp - if len(quickCapture) == 3 { - if b.Verbose { - log.Debugf(log.ExchangeSys, "%s websocket: Successfully subscribed to %s", - b.Name, decodedResp.Subscribe) - } - } else { - b.Websocket.SetCanUseAuthenticatedEndpoints(true) - if b.Verbose { - log.Debugf(log.ExchangeSys, "%s websocket: Successfully authenticated websocket connection", - b.Name) - } - } - continue - } - - b.Websocket.DataHandler <- fmt.Errorf("%s websocket error: Unable to subscribe %s", - b.Name, decodedResp.Subscribe) - } else if _, ok := quickCapture["table"]; ok { - var decodedResp WebsocketMainResponse - err := json.Unmarshal(resp.Raw, &decodedResp) - if err != nil { - b.Websocket.DataHandler <- err - continue - } - - switch decodedResp.Table { - case bitmexWSOrderbookL2: - var orderbooks OrderBookData - err = json.Unmarshal(resp.Raw, &orderbooks) - if err != nil { - b.Websocket.DataHandler <- err - continue - } - - p := currency.NewPairFromString(orderbooks.Data[0].Symbol) - 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 - } - - case bitmexWSTrade: - var trades TradeData - err = json.Unmarshal(resp.Raw, &trades) - if err != nil { - b.Websocket.DataHandler <- err - continue - } - - if trades.Action == bitmexActionInitialData { - continue - } - - for i := range trades.Data { - var timestamp time.Time - timestamp, err = time.Parse(time.RFC3339, trades.Data[i].Timestamp) - if err != nil { - b.Websocket.DataHandler <- err - continue - } - // TODO: update this to support multiple asset types - b.Websocket.DataHandler <- wshandler.TradeData{ - Timestamp: timestamp, - Price: trades.Data[i].Price, - Amount: float64(trades.Data[i].Size), - CurrencyPair: currency.NewPairFromString(trades.Data[i].Symbol), - Exchange: b.Name, - AssetType: "CONTRACT", - Side: trades.Data[i].Side, - } - } - - case bitmexWSAnnouncement: - var announcement AnnouncementData - err = json.Unmarshal(resp.Raw, &announcement) - if err != nil { - b.Websocket.DataHandler <- err - continue - } - - if announcement.Action == bitmexActionInitialData { - continue - } - - b.Websocket.DataHandler <- announcement.Data - case bitmexWSAffiliate: - var response WsAffiliateResponse - err = json.Unmarshal(resp.Raw, &response) - if err != nil { - b.Websocket.DataHandler <- err - continue - } - b.Websocket.DataHandler <- response - case bitmexWSExecution: - var response WsExecutionResponse - err = json.Unmarshal(resp.Raw, &response) - if err != nil { - b.Websocket.DataHandler <- err - continue - } - b.Websocket.DataHandler <- response - case bitmexWSOrder: - var response WsOrderResponse - err = json.Unmarshal(resp.Raw, &response) - if err != nil { - b.Websocket.DataHandler <- err - continue - } - b.Websocket.DataHandler <- response - case bitmexWSMargin: - var response WsMarginResponse - err = json.Unmarshal(resp.Raw, &response) - if err != nil { - b.Websocket.DataHandler <- err - continue - } - b.Websocket.DataHandler <- response - case bitmexWSPosition: - var response WsPositionResponse - err = json.Unmarshal(resp.Raw, &response) - if err != nil { - b.Websocket.DataHandler <- err - continue - } - b.Websocket.DataHandler <- response - case bitmexWSPrivateNotifications: - var response WsPrivateNotificationsResponse - err = json.Unmarshal(resp.Raw, &response) - if err != nil { - b.Websocket.DataHandler <- err - continue - } - b.Websocket.DataHandler <- response - case bitmexWSTransact: - var response WsTransactResponse - err = json.Unmarshal(resp.Raw, &response) - if err != nil { - b.Websocket.DataHandler <- err - continue - } - b.Websocket.DataHandler <- response - case bitmexWSWallet: - var response WsWalletResponse - err = json.Unmarshal(resp.Raw, &response) - if err != nil { - b.Websocket.DataHandler <- err - continue - } - b.Websocket.DataHandler <- response - default: - b.Websocket.DataHandler <- fmt.Errorf("%s websocket error: Table unknown - %s", - b.Name, decodedResp.Table) - } } } } } +func (b *Bitmex) wsHandleData(respRaw []byte) error { + quickCapture := make(map[string]interface{}) + err := json.Unmarshal(respRaw, &quickCapture) + if err != nil { + return err + } + + var respError WebsocketErrorResponse + if _, ok := quickCapture["status"]; ok { + err = json.Unmarshal(respRaw, &respError) + if err != nil { + return err + } + } + + if _, ok := quickCapture["success"]; ok { + var decodedResp WebsocketSubscribeResp + err = json.Unmarshal(respRaw, &decodedResp) + if err != nil { + return err + } + + if decodedResp.Success { + if len(quickCapture) == 3 { + if b.Verbose { + log.Debugf(log.ExchangeSys, "%s websocket: Successfully subscribed to %s", + b.Name, decodedResp.Subscribe) + } + } else { + b.Websocket.SetCanUseAuthenticatedEndpoints(true) + if b.Verbose { + log.Debugf(log.ExchangeSys, "%s websocket: Successfully authenticated websocket connection", + b.Name) + } + } + return nil + } + + b.Websocket.DataHandler <- fmt.Errorf("%s websocket error: Unable to subscribe %s", + b.Name, decodedResp.Subscribe) + } else if _, ok := quickCapture["table"]; ok { + var decodedResp WebsocketMainResponse + err = json.Unmarshal(respRaw, &decodedResp) + if err != nil { + return err + } + switch decodedResp.Table { + case bitmexWSOrderbookL2, bitmexWSOrderbookL225, bitmexWSOrderbookL10: + var orderbooks OrderBookData + err = json.Unmarshal(respRaw, &orderbooks) + if err != nil { + return err + } + if len(orderbooks.Data) == 0 { + return fmt.Errorf("%s - Empty orderbook data received: %s", b.Name, respRaw) + } + p := currency.NewPairFromString(orderbooks.Data[0].Symbol) + var a asset.Item + a, err = b.GetPairAssetType(p) + if err != nil { + return err + } + + err = b.processOrderbook(orderbooks.Data, + orderbooks.Action, + p, + a) + if err != nil { + return err + } + + case bitmexWSTrade: + var trades TradeData + err = json.Unmarshal(respRaw, &trades) + if err != nil { + return err + } + + if trades.Action == bitmexActionInitialData { + return nil + } + + for i := range trades.Data { + var a asset.Item + p := currency.NewPairFromString(trades.Data[i].Symbol) + a, err = b.GetPairAssetType(p) + if err != nil { + return err + } + var oSide order.Side + oSide, err = order.StringToOrderSide(trades.Data[i].Side) + if err != nil { + b.Websocket.DataHandler <- order.ClassificationError{ + Exchange: b.Name, + Err: err, + } + } + + b.Websocket.DataHandler <- wshandler.TradeData{ + Timestamp: trades.Data[i].Timestamp, + Price: trades.Data[i].Price, + Amount: float64(trades.Data[i].Size), + CurrencyPair: p, + Exchange: b.Name, + AssetType: a, + Side: oSide, + } + } + + case bitmexWSAnnouncement: + var announcement AnnouncementData + err = json.Unmarshal(respRaw, &announcement) + if err != nil { + return err + } + + if announcement.Action == bitmexActionInitialData { + return nil + } + + b.Websocket.DataHandler <- announcement.Data + case bitmexWSAffiliate: + var response WsAffiliateResponse + err = json.Unmarshal(respRaw, &response) + if err != nil { + return err + } + b.Websocket.DataHandler <- response + case bitmexWSInstrument: + // ticker + case bitmexWSExecution: + // trades of an order + var response WsExecutionResponse + err = json.Unmarshal(respRaw, &response) + if err != nil { + return err + } + + for i := range response.Data { + p := currency.NewPairFromString(response.Data[i].Symbol) + var a asset.Item + a, err = b.GetPairAssetType(p) + if err != nil { + return err + } + var oStatus order.Status + oStatus, err = order.StringToOrderStatus(response.Data[i].OrdStatus) + if err != nil { + b.Websocket.DataHandler <- order.ClassificationError{ + Exchange: b.Name, + OrderID: response.Data[i].OrderID, + Err: err, + } + } + var oSide order.Side + oSide, err = order.StringToOrderSide(response.Data[i].Side) + if err != nil { + b.Websocket.DataHandler <- order.ClassificationError{ + Exchange: b.Name, + OrderID: response.Data[i].OrderID, + Err: err, + } + } + b.Websocket.DataHandler <- &order.Modify{ + Exchange: b.Name, + ID: response.Data[i].OrderID, + AccountID: strconv.FormatInt(response.Data[i].Account, 10), + AssetType: a, + Pair: p, + Status: oStatus, + Trades: []order.TradeHistory{ + { + Price: response.Data[i].Price, + Amount: response.Data[i].OrderQuantity, + Exchange: b.Name, + TID: response.Data[i].ExecID, + Side: oSide, + Timestamp: response.Data[i].Timestamp, + IsMaker: false, + }, + }, + } + } + case bitmexWSOrder: + var response WsOrderResponse + err = json.Unmarshal(respRaw, &response) + if err != nil { + return err + } + switch response.Action { + case "update", "insert": + for x := range response.Data { + var p currency.Pair + var a asset.Item + p, a, err = b.GetRequestFormattedPairAndAssetType(response.Data[x].Symbol) + if err != nil { + return err + } + var oSide order.Side + oSide, err = order.StringToOrderSide(response.Data[x].Side) + if err != nil { + b.Websocket.DataHandler <- order.ClassificationError{ + Exchange: b.Name, + OrderID: response.Data[x].OrderID, + Err: err, + } + } + var oType order.Type + oType, err = order.StringToOrderType(response.Data[x].OrderType) + if err != nil { + b.Websocket.DataHandler <- order.ClassificationError{ + Exchange: b.Name, + OrderID: response.Data[x].OrderID, + Err: err, + } + } + var oStatus order.Status + oStatus, err = order.StringToOrderStatus(response.Data[x].OrderStatus) + if err != nil { + b.Websocket.DataHandler <- order.ClassificationError{ + Exchange: b.Name, + OrderID: response.Data[x].OrderID, + Err: err, + } + } + b.Websocket.DataHandler <- &order.Detail{ + Price: response.Data[x].Price, + Amount: response.Data[x].OrderQuantity, + Exchange: b.Name, + ID: response.Data[x].OrderID, + AccountID: strconv.FormatInt(response.Data[x].Account, 10), + Type: oType, + Side: oSide, + Status: oStatus, + AssetType: a, + Date: response.Data[x].TransactTime, + Pair: p, + } + } + case "delete": + for x := range response.Data { + var p currency.Pair + var a asset.Item + p, a, err = b.GetRequestFormattedPairAndAssetType(response.Data[x].Symbol) + if err != nil { + return err + } + var oSide order.Side + oSide, err = order.StringToOrderSide(response.Data[x].Side) + if err != nil { + b.Websocket.DataHandler <- order.ClassificationError{ + Exchange: b.Name, + OrderID: response.Data[x].OrderID, + Err: err, + } + } + var oType order.Type + oType, err = order.StringToOrderType(response.Data[x].OrderType) + if err != nil { + b.Websocket.DataHandler <- order.ClassificationError{ + Exchange: b.Name, + OrderID: response.Data[x].OrderID, + Err: err, + } + } + var oStatus order.Status + oStatus, err = order.StringToOrderStatus(response.Data[x].OrderStatus) + if err != nil { + b.Websocket.DataHandler <- order.ClassificationError{ + Exchange: b.Name, + OrderID: response.Data[x].OrderID, + Err: err, + } + } + b.Websocket.DataHandler <- &order.Cancel{ + Price: response.Data[x].Price, + Amount: response.Data[x].OrderQuantity, + Exchange: b.Name, + ID: response.Data[x].OrderID, + AccountID: strconv.FormatInt(response.Data[x].Account, 10), + Type: oType, + Side: oSide, + Status: oStatus, + AssetType: a, + Date: response.Data[x].TransactTime, + Pair: p, + } + } + default: + b.Websocket.DataHandler <- fmt.Errorf("%s - Unsupported order update %+v", b.Name, response) + } + case bitmexWSMargin: + var response WsMarginResponse + err = json.Unmarshal(respRaw, &response) + if err != nil { + return err + } + b.Websocket.DataHandler <- response + case bitmexWSPosition: + var response WsPositionResponse + err = json.Unmarshal(respRaw, &response) + if err != nil { + return err + } + + case bitmexWSPrivateNotifications: + var response WsPrivateNotificationsResponse + err = json.Unmarshal(respRaw, &response) + if err != nil { + return err + } + b.Websocket.DataHandler <- response + case bitmexWSTransact: + var response WsTransactResponse + err = json.Unmarshal(respRaw, &response) + if err != nil { + return err + } + b.Websocket.DataHandler <- response + case bitmexWSWallet: + var response WsWalletResponse + err = json.Unmarshal(respRaw, &response) + if err != nil { + return err + } + b.Websocket.DataHandler <- response + default: + b.Websocket.DataHandler <- wshandler.UnhandledMessageWarning{Message: b.Name + wshandler.UnhandledMessage + string(respRaw)} + return nil + } + } + return nil +} + // ProcessOrderbook processes orderbook updates func (b *Bitmex) processOrderbook(data []OrderBookL2, action string, currencyPair currency.Pair, assetType asset.Item) error { // nolint: unparam if len(data) < 1 { diff --git a/exchanges/bitmex/bitmex_websocket_types.go b/exchanges/bitmex/bitmex_websocket_types.go index 12f9f053..26a2a032 100644 --- a/exchanges/bitmex/bitmex_websocket_types.go +++ b/exchanges/bitmex/bitmex_websocket_types.go @@ -1,5 +1,7 @@ package bitmex +import "time" + // WebsocketRequest is the main request type type WebsocketRequest struct { Command string `json:"op"` @@ -8,7 +10,7 @@ type WebsocketRequest struct { // WebsocketErrorResponse main error response type WebsocketErrorResponse struct { - Status int `json:"status"` + Status int64 `json:"status"` Error string `json:"error"` Meta interface{} `json:"meta"` Request WebsocketRequest `json:"request"` @@ -51,6 +53,7 @@ type WebsocketMainResponse struct { ID string `json:"id"` Symbol string `json:"symbol"` } `json:"Attributes"` + Action string `json:"action,omitempty"` } // OrderBookData contains orderbook resp data with action to be taken @@ -97,7 +100,58 @@ type WsOrderResponse struct { ForeignKeys WsOrderResponseForeignKeys `json:"foreignKeys"` Attributes WsOrderResponseAttributes `json:"attributes"` Filter WsOrderResponseFilter `json:"filter"` - Data []interface{} `json:"data"` + Data []OrderInsertData `json:"data"` +} + +// OrderInsertData holds order data from an order response +type OrderInsertData struct { + WorkingIndicator bool `json:"workingIndicator"` + Account int64 `json:"account"` + AveragePrice float64 `json:"avgPx"` + Commission float64 `json:"commission"` + FilledQuantity float64 `json:"cumQty"` + DisplayQuantity float64 `json:"displayQty"` + ExecComm float64 `json:"execComm"` + ExecCost float64 `json:"execCost"` + ForeignNotional float64 `json:"foreignNotional"` + HomeNotional float64 `json:"homeNotional"` + LastPrice float64 `json:"lastPx"` + LastQuantity float64 `json:"lastQty"` + LeavesQuantity float64 `json:"leavesQty"` + OrderQuantity float64 `json:"orderQty"` + PegOffsetValue float64 `json:"pegOffsetValue"` + Price float64 `json:"price"` + SimpleFilledQuantity float64 `json:"simpleCumQty"` + SimpleLeavesQuantity float64 `json:"simpleLeavesQty"` + SimpleOrderQuantity float64 `json:"simpleOrderQty"` + StopPrice float64 `json:"stopPx"` + ExDestination string `json:"exDestination"` + ContingencyType string `json:"contingencyType"` + Currency string `json:"currency"` + ExecutionID string `json:"execID"` + ExecutionInstance string `json:"execInst"` + ExecutionType string `json:"execType"` + LastLiquidityInd string `json:"lastLiquidityInd"` + LastMkt string `json:"lastMkt"` + UnderlyingLastPrice float64 `json:"underlyingLastPx"` + MultiLegReportingType string `json:"multiLegReportingType"` + OrderRejectedReason string `json:"ordRejReason"` + OrderStatus string `json:"ordStatus"` + OrderType string `json:"ordType"` + OrderID string `json:"orderID"` + PegPriceType string `json:"pegPriceType"` + ClientOrderID string `json:"clOrdID"` + ClientOrderLinkID string `json:"clOrdLinkID"` + Symbol string `json:"symbol"` + Text string `json:"text"` + TimeInForce string `json:"timeInForce"` + Timestamp time.Time `json:"timestamp"` + TradePublishIndicator string `json:"tradePublishIndicator"` + TransactTime time.Time `json:"transactTime"` + TradingMatchID string `json:"trdMatchID"` + Triggered string `json:"triggered"` + SettleCurrency string `json:"settlCurrency"` + Side string `json:"side"` } // WsOrderResponseAttributes private api data @@ -195,7 +249,57 @@ type WsExecutionResponse struct { ForeignKeys WsExecutionResponseForeignKeys `json:"foreignKeys"` Attributes WsExecutionResponseAttributes `json:"attributes"` Filter WsExecutionResponseFilter `json:"filter"` - Data []interface{} `json:"data"` + Data []wsExecutionData `json:"data"` +} + +type wsExecutionData struct { + WorkingIndicator bool `json:"workingIndicator"` + Account int64 `json:"account"` + AvgPx float64 `json:"avgPx"` + Commission float64 `json:"commission"` + FilledQuantity float64 `json:"cumQty"` + DisplayQuantity float64 `json:"displayQty"` + ExecComm float64 `json:"execComm"` + ExecCost float64 `json:"execCost"` + ForeignNotional float64 `json:"foreignNotional"` + HomeNotional float64 `json:"homeNotional"` + LastPx float64 `json:"lastPx"` + LastQuantity float64 `json:"lastQty"` + LeavesQuantity float64 `json:"leavesQty"` + OrderQuantity float64 `json:"orderQty"` + PegOffsetValue float64 `json:"pegOffsetValue"` + Price float64 `json:"price"` + SimpleFilledQuantity float64 `json:"simpleCumQty"` + SimpleLeavesQuantity float64 `json:"simpleLeavesQty"` + SimpleOrderQuantity float64 `json:"simpleOrderQty"` + StopPx float64 `json:"stopPx"` + UnderlyingLastPx float64 `json:"underlyingLastPx"` + PegPriceType string `json:"pegPriceType"` + Symbol string `json:"symbol"` + Text string `json:"text"` + TimeInForce string `json:"timeInForce"` + Timestamp time.Time `json:"timestamp"` + TradePublishIndicator string `json:"tradePublishIndicator"` + TransactTime time.Time `json:"transactTime"` + TrdMatchID string `json:"trdMatchID"` + Triggered string `json:"triggered"` + ClOrdID string `json:"clOrdID"` + ClOrdLinkID string `json:"clOrdLinkID"` + SettlCurrency string `json:"settlCurrency"` + Side string `json:"side"` + MultiLegReportingType string `json:"multiLegReportingType"` + OrdRejReason string `json:"ordRejReason"` + OrdStatus string `json:"ordStatus"` + OrdType string `json:"ordType"` + OrderID string `json:"orderID"` + LastLiquidityInd string `json:"lastLiquidityInd"` + LastMkt string `json:"lastMkt"` + ExecID string `json:"execID"` + ExecInst string `json:"execInst"` + ExecType string `json:"execType"` + ExDestination string `json:"exDestination"` + Currency string `json:"currency"` + ContingencyType string `json:"contingencyType"` } // WsExecutionResponseAttributes private api data @@ -282,7 +386,7 @@ type WsMarginResponseData struct { ExcessMarginPcnt float64 `json:"excessMarginPcnt"` AvailableMargin float64 `json:"availableMargin"` WithdrawableMargin float64 `json:"withdrawableMargin"` - Timestamp string `json:"timestamp"` + Timestamp time.Time `json:"timestamp"` GrossLastValue float64 `json:"grossLastValue"` Commission interface{} `json:"commission"` } @@ -298,7 +402,58 @@ type WsPositionResponse struct { ForeignKeys WsPositionResponseForeignKeys `json:"foreignKeys"` Attributes WsPositionResponseAttributes `json:"attributes"` Filter WsPositionResponseFilter `json:"filter"` - Data []interface{} `json:"data"` + Data []wsPositionData `json:"data"` +} + +type wsPositionData struct { + IsOpen bool `json:"isOpen"` + Account int64 `json:"account"` + CurrentQuantity float64 `json:"currentQty"` + HomeNotional float64 `json:"homeNotional"` + LiquidationPrice float64 `json:"liquidationPrice"` + MaintMargin float64 `json:"maintMargin"` + MarkPrice float64 `json:"markPrice"` + MarkValue float64 `json:"markValue"` + RiskValue float64 `json:"riskValue"` + SimpleQuantity float64 `json:"simpleQty"` + UnrealisedGrossPnl float64 `json:"unrealisedGrossPnl"` + UnrealisedPnl float64 `json:"unrealisedPnl"` + UnrealisedPnlPcnt float64 `json:"unrealisedPnlPcnt"` + UnrealisedRoePcnt float64 `json:"unrealisedRoePcnt"` + BankruptPrice float64 `json:"bankruptPrice"` + AvgCostPrice float64 `json:"avgCostPrice"` + AvgEntryPrice float64 `json:"avgEntryPrice"` + BreakEvenPrice float64 `json:"breakEvenPrice"` + CurrentComm float64 `json:"currentComm"` + CurrentCost float64 `json:"currentCost"` + DeleveragePercentile float64 `json:"deleveragePercentile"` + ExecComm float64 `json:"execComm"` + ExecCost float64 `json:"execCost"` + ExecQuantity float64 `json:"execQty"` + ExecSellCost float64 `json:"execSellCost"` + ExecSellQuantity float64 `json:"execSellQty"` + ForeignNotional float64 `json:"foreignNotional"` + GrossExecCost float64 `json:"grossExecCost"` + MarginCallPrice float64 `json:"marginCallPrice"` + PosComm float64 `json:"posComm"` + PosCost float64 `json:"posCost"` + PosCost2 float64 `json:"posCost2"` + PosInit float64 `json:"posInit"` + PosMaint float64 `json:"posMaint"` + PosMargin float64 `json:"posMargin"` + PrevRealisedPnl float64 `json:"prevRealisedPnl"` + RealisedCost float64 `json:"realisedCost"` + RealisedGrossPnl float64 `json:"realisedGrossPnl"` + RealisedPnl float64 `json:"realisedPnl"` + RebalancedPnl float64 `json:"rebalancedPnl"` + SimpleCost float64 `json:"simpleCost"` + SimpleValue float64 `json:"simpleValue"` + UnrealisedCost float64 `json:"unrealisedCost"` + Currency string `json:"currency"` + CurrentTimestamp time.Time `json:"currentTimestamp"` + Symbol string `json:"symbol"` + Timestamp time.Time `json:"timestamp"` + PosState string `json:"posState"` } // WsPositionResponseAttributes private api data diff --git a/exchanges/bitmex/bitmex_wrapper.go b/exchanges/bitmex/bitmex_wrapper.go index 0afce123..b4e5a6ee 100644 --- a/exchanges/bitmex/bitmex_wrapper.go +++ b/exchanges/bitmex/bitmex_wrapper.go @@ -122,6 +122,8 @@ func (b *Bitmex) SetDefaults() { AuthenticatedEndpoints: true, AccountInfo: true, DeadMansSwitch: true, + GetOrders: true, + GetOrder: true, }, WithdrawPermissions: exchange.AutoWithdrawCryptoWithAPIPermission | exchange.WithdrawCryptoWithEmail | @@ -222,7 +224,7 @@ func (b *Bitmex) Run() { } // FetchTradablePairs returns a list of the exchanges tradable pairs -func (b *Bitmex) FetchTradablePairs(asset asset.Item) ([]string, error) { +func (b *Bitmex) FetchTradablePairs(_ asset.Item) ([]string, error) { marketInfo, err := b.GetActiveInstruments(&GenericRequestParams{}) if err != nil { return nil, err @@ -435,13 +437,13 @@ func (b *Bitmex) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) { } var orderNewParams = OrderNewParams{ - OrdType: s.OrderSide.String(), - Symbol: s.Pair.String(), - OrderQty: s.Amount, - Side: s.OrderSide.String(), + OrderType: s.Side.String(), + Symbol: s.Pair.String(), + OrderQuantity: s.Amount, + Side: s.Side.String(), } - if s.OrderType == order.Limit { + if s.Type == order.Limit { orderNewParams.Price = s.Price } @@ -452,7 +454,7 @@ func (b *Bitmex) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) { if response.OrderID != "" { submitOrderResponse.OrderID = response.OrderID } - if s.OrderType == order.Market { + if s.Type == order.Market { submitOrderResponse.FullyMatched = true } submitOrderResponse.IsOrderPlaced = true @@ -469,7 +471,7 @@ func (b *Bitmex) ModifyOrder(action *order.Modify) (string, error) { return "", errors.New("contract amount can not have decimals") } - params.OrderID = action.OrderID + params.OrderID = action.ID params.OrderQty = int32(action.Amount) params.Price = action.Price @@ -484,7 +486,7 @@ func (b *Bitmex) ModifyOrder(action *order.Modify) (string, error) { // CancelOrder cancels an order by its corresponding ID number func (b *Bitmex) CancelOrder(order *order.Cancel) error { var params = OrderCancelParams{ - OrderID: order.OrderID, + OrderID: order.ID, } _, err := b.CancelOrders(¶ms) return err @@ -587,18 +589,18 @@ func (b *Bitmex) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, e orderSide := orderSideMap[resp[i].Side] orderType := orderTypeMap[resp[i].OrdType] if orderType == "" { - orderType = order.Unknown + orderType = order.UnknownType } orderDetail := order.Detail{ - Price: resp[i].Price, - Amount: float64(resp[i].OrderQty), - Exchange: b.Name, - ID: resp[i].OrderID, - OrderSide: orderSide, - OrderType: orderType, - Status: order.Status(resp[i].OrdStatus), - CurrencyPair: currency.NewPairWithDelimiter(resp[i].Symbol, + Price: resp[i].Price, + Amount: float64(resp[i].OrderQty), + Exchange: b.Name, + ID: resp[i].OrderID, + Side: orderSide, + Type: orderType, + Status: order.Status(resp[i].OrdStatus), + Pair: currency.NewPairWithDelimiter(resp[i].Symbol, resp[i].SettlCurrency, b.GetPairFormat(asset.PerpetualContract, false).Delimiter), } @@ -606,10 +608,10 @@ func (b *Bitmex) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, e orders = append(orders, orderDetail) } - order.FilterOrdersBySide(&orders, req.OrderSide) - order.FilterOrdersByType(&orders, req.OrderType) + order.FilterOrdersBySide(&orders, req.Side) + order.FilterOrdersByType(&orders, req.Type) order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) - order.FilterOrdersByCurrencies(&orders, req.Currencies) + order.FilterOrdersByCurrencies(&orders, req.Pairs) return orders, nil } @@ -628,18 +630,18 @@ func (b *Bitmex) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail, e orderSide := orderSideMap[resp[i].Side] orderType := orderTypeMap[resp[i].OrdType] if orderType == "" { - orderType = order.Unknown + orderType = order.UnknownType } orderDetail := order.Detail{ - Price: resp[i].Price, - Amount: float64(resp[i].OrderQty), - Exchange: b.Name, - ID: resp[i].OrderID, - OrderSide: orderSide, - OrderType: orderType, - Status: order.Status(resp[i].OrdStatus), - CurrencyPair: currency.NewPairWithDelimiter(resp[i].Symbol, + Price: resp[i].Price, + Amount: float64(resp[i].OrderQty), + Exchange: b.Name, + ID: resp[i].OrderID, + Side: orderSide, + Type: orderType, + Status: order.Status(resp[i].OrdStatus), + Pair: currency.NewPairWithDelimiter(resp[i].Symbol, resp[i].SettlCurrency, b.GetPairFormat(asset.PerpetualContract, false).Delimiter), } @@ -647,10 +649,10 @@ func (b *Bitmex) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail, e orders = append(orders, orderDetail) } - order.FilterOrdersBySide(&orders, req.OrderSide) - order.FilterOrdersByType(&orders, req.OrderType) + order.FilterOrdersBySide(&orders, req.Side) + order.FilterOrdersByType(&orders, req.Type) order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) - order.FilterOrdersByCurrencies(&orders, req.Currencies) + order.FilterOrdersByCurrencies(&orders, req.Pairs) return orders, nil } diff --git a/exchanges/bitstamp/bitstamp_live_test.go b/exchanges/bitstamp/bitstamp_live_test.go index 66d0ef87..5c6cebb0 100644 --- a/exchanges/bitstamp/bitstamp_live_test.go +++ b/exchanges/bitstamp/bitstamp_live_test.go @@ -34,6 +34,8 @@ func TestMain(m *testing.M) { if err != nil { log.Fatal("Bitstamp setup error", err) } + b.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride() + b.Websocket.TrafficAlert = sharedtestvalues.GetWebsocketStructChannelOverride() log.Printf(sharedtestvalues.LiveTesting, b.Name, b.API.Endpoints.URL) os.Exit(m.Run()) } diff --git a/exchanges/bitstamp/bitstamp_mock_test.go b/exchanges/bitstamp/bitstamp_mock_test.go index ffb3943f..136f8a85 100644 --- a/exchanges/bitstamp/bitstamp_mock_test.go +++ b/exchanges/bitstamp/bitstamp_mock_test.go @@ -46,7 +46,8 @@ func TestMain(m *testing.M) { b.HTTPClient = newClient b.API.Endpoints.URL = serverDetails + "/api" - + b.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride() + b.Websocket.TrafficAlert = sharedtestvalues.GetWebsocketStructChannelOverride() log.Printf(sharedtestvalues.MockTesting, b.Name, b.API.Endpoints.URL) os.Exit(m.Run()) } diff --git a/exchanges/bitstamp/bitstamp_test.go b/exchanges/bitstamp/bitstamp_test.go index 5bad3a19..fc8b8b68 100644 --- a/exchanges/bitstamp/bitstamp_test.go +++ b/exchanges/bitstamp/bitstamp_test.go @@ -334,7 +334,7 @@ func TestGetActiveOrders(t *testing.T) { t.Parallel() var getOrdersRequest = order.GetOrdersRequest{ - OrderType: order.AnyType, + Type: order.AnyType, } _, err := b.GetActiveOrders(&getOrdersRequest) @@ -352,7 +352,7 @@ func TestGetOrderHistory(t *testing.T) { t.Parallel() var getOrdersRequest = order.GetOrdersRequest{ - OrderType: order.AnyType, + Type: order.AnyType, } _, err := b.GetOrderHistory(&getOrdersRequest) @@ -381,11 +381,11 @@ func TestSubmitOrder(t *testing.T) { Base: currency.BTC, Quote: currency.USD, }, - OrderSide: order.Buy, - OrderType: order.Limit, - Price: 1, - Amount: 1, - ClientID: "meowOrder", + Side: order.Buy, + Type: order.Limit, + Price: 1, + Amount: 1, + ClientID: "meowOrder", } response, err := b.SubmitOrder(orderSubmission) switch { @@ -406,7 +406,7 @@ func TestCancelExchangeOrder(t *testing.T) { } orderCancellation := &order.Cancel{ - OrderID: "1234", + ID: "1234", } err := b.CancelOrder(orderCancellation) switch { @@ -594,3 +594,73 @@ func TestParseTime(t *testing.T) { t.Error("invalid time values") } } + +func TestWsSubscription(t *testing.T) { + pressXToJSON := []byte(`{ + "event": "bts:subscribe", + "data": { + "channel": "[channel_name]" + } + }`) + err := b.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsUnsubscribe(t *testing.T) { + pressXToJSON := []byte(`{ + "event": "bts:subscribe", + "data": { + "channel": "[channel_name]" + } + }`) + err := b.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsTrade(t *testing.T) { + pressXToJSON := []byte(`{"data": {"microtimestamp": "1580336751488517", "amount": 0.00598803, "buy_order_id": 4621328909, "sell_order_id": 4621329035, "amount_str": "0.00598803", "price_str": "9334.73", "timestamp": "1580336751", "price": 9334.73, "type": 1, "id": 104007706}, "event": "trade", "channel": "live_trades_btcusd"}`) + err := b.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsOrderbook(t *testing.T) { + pressXToJSON := []byte(`{"data": {"timestamp": "1580336834", "microtimestamp": "1580336834607546", "bids": [["9328.28", "0.05925332"], ["9327.34", "0.43120000"], ["9327.29", "0.63470860"], ["9326.59", "0.41114619"], ["9326.38", "1.06910000"], ["9323.91", "2.67930000"], ["9322.69", "0.80000000"], ["9322.57", "0.03000000"], ["9322.31", "1.36010820"], ["9319.54", "0.03090000"], ["9318.97", "0.28000000"], ["9317.61", "0.02910000"], ["9316.39", "1.08000000"], ["9316.20", "2.00000000"], ["9315.48", "1.00000000"], ["9314.72", "0.11197459"], ["9314.47", "0.32207398"], ["9312.53", "0.03961501"], ["9312.29", "1.00000000"], ["9311.78", "0.03060000"], ["9311.69", "0.32217221"], ["9310.98", "3.29000000"], ["9310.18", "0.01304192"], ["9310.13", "0.02500000"], ["9309.04", "1.00000000"], ["9309.00", "0.05000000"], ["9308.96", "0.03030000"], ["9308.91", "0.32227154"], ["9307.52", "0.32191362"], ["9307.25", "2.44280000"], ["9305.92", "3.00000000"], ["9305.62", "2.37600000"], ["9305.60", "0.21815312"], ["9305.54", "2.80000000"], ["9305.13", "0.05000000"], ["9305.02", "2.90917302"], ["9303.68", "0.02316372"], ["9303.53", "12.55000000"], ["9303.00", "0.02191430"], ["9302.94", "2.38250000"], ["9302.37", "0.01000000"], ["9301.85", "2.50000000"], ["9300.89", "0.02000000"], ["9300.40", "4.10000000"], ["9300.00", "0.33936139"], ["9298.48", "1.45200000"], ["9297.80", "0.42380000"], ["9295.44", "4.54689328"], ["9295.43", "3.20000000"], ["9295.00", "0.28669566"], ["9291.66", "14.09931321"], ["9290.13", "2.87254900"], ["9290.00", "0.67530840"], ["9285.37", "0.38033002"], ["9285.15", "5.37993528"], ["9285.00", "0.09419278"], ["9283.71", "0.15679830"], ["9280.33", "12.55000000"], ["9280.13", "3.20310000"], ["9280.00", "1.36477909"], ["9276.01", "0.00707488"], ["9275.75", "0.56974291"], ["9275.00", "5.88000000"], ["9274.00", "0.00754205"], ["9271.68", "0.01400000"], ["9271.11", "15.37188500"], ["9270.00", "0.06674325"], ["9268.79", "24.54320000"], ["9257.18", "12.55000000"], ["9256.30", "0.17876365"], ["9255.71", "13.82642967"], ["9254.79", "0.96329407"], ["9250.00", "0.78214958"], ["9245.34", "4.90200000"], ["9245.13", "0.10000000"], ["9240.00", "0.44383459"], ["9238.84", "13.16615207"], ["9234.11", "0.43317656"], ["9234.10", "12.55000000"], ["9231.28", "11.79290000"], ["9230.09", "4.15059441"], ["9227.69", "0.00791097"], ["9225.00", "0.44768346"], ["9224.49", "0.85857203"], ["9223.50", "5.61001041"], ["9216.01", "0.03222653"], ["9216.00", "0.05000000"], ["9213.54", "0.71253866"], ["9212.50", "2.86768195"], ["9211.07", "12.55000000"], ["9210.00", "0.54288817"], ["9208.00", "1.00000000"], ["9206.06", "2.62587578"], ["9205.98", "15.40000000"], ["9205.52", "0.01710603"], ["9205.37", "0.03524953"], ["9205.11", "0.15000000"], ["9205.00", "0.01534763"], ["9204.76", "7.00600000"], ["9203.00", "0.01090000"]], "asks": [["9337.10", "0.03000000"], ["9340.85", "2.67820000"], ["9340.95", "0.02900000"], ["9341.17", "1.00000000"], ["9341.41", "2.13966390"], ["9341.61", "0.20000000"], ["9341.97", "0.11199911"], ["9341.98", "3.00000000"], ["9342.26", "0.32112762"], ["9343.87", "1.00000000"], ["9344.17", "3.57250000"], ["9345.04", "0.32103450"], ["9345.41", "4.90000000"], ["9345.69", "1.03000000"], ["9345.80", "0.03000000"], ["9346.00", "0.10200000"], ["9346.69", "0.02397394"], ["9347.41", "1.00000000"], ["9347.82", "0.32094177"], ["9348.23", "0.02880000"], ["9348.62", "11.96287551"], ["9349.31", "2.44270000"], ["9349.47", "0.96000000"], ["9349.86", "4.50000000"], ["9350.37", "0.03300000"], ["9350.57", "0.34682266"], ["9350.60", "0.32085527"], ["9351.45", "0.31147923"], ["9352.31", "0.28000000"], ["9352.86", "9.80000000"], ["9353.73", "0.02360739"], ["9354.00", "0.45000000"], ["9354.12", "0.03000000"], ["9354.29", "3.82446861"], ["9356.20", "0.64000000"], ["9356.90", "0.02316372"], ["9357.30", "2.50000000"], ["9357.70", "2.38240000"], ["9358.92", "6.00000000"], ["9359.97", "0.34898075"], ["9359.98", "2.30000000"], ["9362.56", "2.37600000"], ["9365.00", "0.64000000"], ["9365.16", "1.70030306"], ["9365.27", "3.03000000"], ["9369.99", "2.47102665"], ["9370.00", "3.15688574"], ["9370.21", "2.32720000"], ["9371.78", "13.20000000"], ["9371.89", "0.96293482"], ["9375.08", "4.74762500"], ["9384.34", "1.45200000"], ["9384.49", "16.42310000"], ["9385.66", "0.34382112"], ["9388.19", "0.00268265"], ["9392.20", "0.20980000"], ["9392.40", "0.10320000"], ["9393.00", "0.20980000"], ["9395.40", "0.40000000"], ["9398.86", "24.54310000"], ["9400.00", "0.05489988"], ["9400.33", "0.00495100"], ["9400.45", "0.00484700"], ["9402.92", "17.20000000"], ["9404.18", "10.00000000"], ["9418.89", "16.38000000"], ["9419.41", "3.06700000"], ["9420.40", "12.50000000"], ["9421.11", "0.10500000"], ["9434.47", "0.03215805"], ["9434.48", "0.28285714"], ["9434.49", "15.83000000"], ["9435.13", "0.15000000"], ["9438.93", "0.00368800"], ["9439.19", "0.69343985"], ["9442.86", "0.10000000"], ["9443.96", "12.50000000"], ["9444.00", "0.06004471"], ["9444.97", "0.01494896"], ["9447.00", "0.01234000"], ["9448.97", "0.14500000"], ["9449.00", "0.05000000"], ["9450.00", "11.13426018"], ["9451.87", "15.90000000"], ["9452.00", "0.20000000"], ["9454.25", "0.01100000"], ["9454.51", "0.02409062"], ["9455.05", "0.00600063"], ["9456.00", "0.27965118"], ["9456.10", "0.17000000"], ["9459.00", "0.00320000"], ["9459.98", "0.02460685"], ["9459.99", "8.11000000"], ["9460.00", "0.08500000"], ["9464.36", "0.56957951"], ["9464.54", "0.69158059"], ["9465.00", "21.00002015"], ["9467.57", "12.50000000"], ["9468.00", "0.08800000"], ["9469.09", "13.94000000"]]}, "event": "data", "channel": "order_book_btcusd"}`) + err := b.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsOrderbook2(t *testing.T) { + pressXToJSON := []byte(`{"data": {"timestamp": "1580336904", "microtimestamp": "1580336904228758", "bids": [["9317.09", "2.67910000"], ["9296.14", "0.00000000"], ["9294.36", "0.04967421"]], "asks": [["9333.85", "0.00000000"], ["9339.20", "0.20000000"], ["9339.66", "0.00000000"], ["9342.63", "4.90000000"], ["9343.66", "0.00000000"], ["9343.76", "2.87275947"]]}, "event": "data", "channel": "diff_order_book_btcusd"}`) + err := b.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsOrderUpdate(t *testing.T) { + pressXToJSON := []byte(`{"data": {"microtimestamp": "1580336940972599", "amount": 0.6347086, "order_type": 0, "amount_str": "0.63470860", "price_str": "9350.49", "price": 9350.49, "id": 4621332237, "datetime": "1580336940"}, "event": "order_created", "channel": "live_orders_btcusd"}`) + err := b.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsRequestReconnect(t *testing.T) { + pressXToJSON := []byte(`{ + "event": "bts:request_reconnect", + "channel": "", + "data": "" + }`) + err := b.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} diff --git a/exchanges/bitstamp/bitstamp_types.go b/exchanges/bitstamp/bitstamp_types.go index 8483211b..e5e42d5e 100644 --- a/exchanges/bitstamp/bitstamp_types.go +++ b/exchanges/bitstamp/bitstamp_types.go @@ -202,7 +202,7 @@ type websocketTradeData struct { SellOrderID int64 `json:"sell_order_id"` AmountStr string `json:"amount_str"` PriceStr string `json:"price_str"` - Timestamp string `json:"timestamp"` + Timestamp int64 `json:"timestamp,string"` Price float64 `json:"price"` Type int `json:"type"` ID int `json:"id"` diff --git a/exchanges/bitstamp/bitstamp_websocket.go b/exchanges/bitstamp/bitstamp_websocket.go index 51c1a1ab..f91d5c88 100644 --- a/exchanges/bitstamp/bitstamp_websocket.go +++ b/exchanges/bitstamp/bitstamp_websocket.go @@ -11,6 +11,7 @@ import ( "github.com/gorilla/websocket" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" "github.com/thrasher-corp/gocryptotrader/log" @@ -33,31 +34,26 @@ func (b *Bitstamp) WsConnect() error { if b.Verbose { log.Debugf(log.ExchangeSys, "%s Connected to Websocket.\n", b.Name) } - err = b.seedOrderBook() if err != nil { b.Websocket.DataHandler <- err } - b.generateDefaultSubscriptions() - go b.WsHandleData() + go b.wsReadData() return nil } -// WsHandleData handles websocket data from WsReadData -func (b *Bitstamp) WsHandleData() { +// wsReadData receives and passes on websocket messages for processing +func (b *Bitstamp) wsReadData() { b.Websocket.Wg.Add(1) - defer func() { b.Websocket.Wg.Done() }() - for { select { case <-b.Websocket.ShutdownC: return - default: resp, err := b.WebsocketConn.ReadMessage() if err != nil { @@ -65,61 +61,84 @@ func (b *Bitstamp) WsHandleData() { return } b.Websocket.TrafficAlert <- struct{}{} - wsResponse := websocketResponse{} - err = json.Unmarshal(resp.Raw, &wsResponse) + err = b.wsHandleData(resp.Raw) if err != nil { b.Websocket.DataHandler <- err - continue - } - - switch wsResponse.Event { - case "bts:request_reconnect": - if b.Verbose { - log.Debugf(log.ExchangeSys, "%v - Websocket reconnection request received", b.Name) - } - go b.Websocket.Shutdown() // Connection monitor will reconnect - - case "data": - wsOrderBookTemp := websocketOrderBookResponse{} - err := json.Unmarshal(resp.Raw, &wsOrderBookTemp) - if err != nil { - b.Websocket.DataHandler <- err - continue - } - - currencyPair := strings.Split(wsResponse.Channel, "_") - p := currency.NewPairFromString(strings.ToUpper(currencyPair[2])) - - err = b.wsUpdateOrderbook(wsOrderBookTemp.Data, p, asset.Spot) - if err != nil { - b.Websocket.DataHandler <- err - continue - } - - case "trade": - wsTradeTemp := websocketTradeResponse{} - - err := json.Unmarshal(resp.Raw, &wsTradeTemp) - if err != nil { - b.Websocket.DataHandler <- err - continue - } - - currencyPair := strings.Split(wsResponse.Channel, "_") - p := currency.NewPairFromString(strings.ToUpper(currencyPair[2])) - - b.Websocket.DataHandler <- wshandler.TradeData{ - Price: wsTradeTemp.Data.Price, - Amount: wsTradeTemp.Data.Amount, - CurrencyPair: p, - Exchange: b.Name, - AssetType: asset.Spot, - } } } } } +func (b *Bitstamp) wsHandleData(respRaw []byte) error { + var wsResponse websocketResponse + err := json.Unmarshal(respRaw, &wsResponse) + if err != nil { + return err + } + + switch wsResponse.Event { + case "bts:subscribe": + if b.Verbose { + log.Debugf(log.ExchangeSys, "%v - Websocket subscription acknowledgement", b.Name) + } + case "bts:unsubscribe": + if b.Verbose { + log.Debugf(log.ExchangeSys, "%v - Websocket unsubscribe acknowledgement", b.Name) + } + case "bts:request_reconnect": + if b.Verbose { + log.Debugf(log.ExchangeSys, "%v - Websocket reconnection request received", b.Name) + } + go b.Websocket.Shutdown() // Connection monitor will reconnect + case "data": + wsOrderBookTemp := websocketOrderBookResponse{} + err := json.Unmarshal(respRaw, &wsOrderBookTemp) + if err != nil { + return err + } + currencyPair := strings.Split(wsResponse.Channel, "_") + p := currency.NewPairFromString(strings.ToUpper(currencyPair[2])) + err = b.wsUpdateOrderbook(wsOrderBookTemp.Data, p, asset.Spot) + if err != nil { + return err + } + case "trade": + wsTradeTemp := websocketTradeResponse{} + err := json.Unmarshal(respRaw, &wsTradeTemp) + if err != nil { + return err + } + currencyPair := strings.Split(wsResponse.Channel, "_") + p := currency.NewPairFromString(strings.ToUpper(currencyPair[2])) + side := order.Buy + if wsTradeTemp.Data.Type == -1 { + side = order.Sell + } + var a asset.Item + a, err = b.GetPairAssetType(p) + if err != nil { + return err + } + b.Websocket.DataHandler <- wshandler.TradeData{ + Timestamp: time.Unix(wsTradeTemp.Data.Timestamp, 0), + CurrencyPair: p, + AssetType: a, + Exchange: b.Name, + EventType: order.UnknownType, + Price: wsTradeTemp.Data.Price, + Amount: wsTradeTemp.Data.Amount, + Side: side, + } + case "order_created", "order_deleted", "order_changed": + if b.Verbose { + log.Debugf(log.ExchangeSys, "%v - Websocket order acknowledgement", b.Name) + } + default: + b.Websocket.DataHandler <- wshandler.UnhandledMessageWarning{Message: b.Name + wshandler.UnhandledMessage + string(respRaw)} + } + return nil +} + func (b *Bitstamp) generateDefaultSubscriptions() { var channels = []string{"live_trades_", "order_book_"} enabledCurrencies := b.GetEnabledPairs(asset.Spot) @@ -160,7 +179,6 @@ func (b *Bitstamp) wsUpdateOrderbook(update websocketOrderBook, p currency.Pair, if len(update.Asks) == 0 && len(update.Bids) == 0 { return errors.New("bitstamp_websocket.go error - no orderbook data") } - var asks, bids []orderbook.Item for i := range update.Asks { target, err := strconv.ParseFloat(update.Asks[i][0], 64) @@ -168,23 +186,19 @@ func (b *Bitstamp) wsUpdateOrderbook(update websocketOrderBook, p currency.Pair, 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.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 @@ -193,7 +207,6 @@ func (b *Bitstamp) wsUpdateOrderbook(update websocketOrderBook, p currency.Pair, bids = append(bids, orderbook.Item{Price: target, Amount: amount}) } - err := b.Websocket.Orderbook.LoadSnapshot(&orderbook.Base{ Bids: bids, Asks: asks, diff --git a/exchanges/bitstamp/bitstamp_wrapper.go b/exchanges/bitstamp/bitstamp_wrapper.go index 7112f240..7f899918 100644 --- a/exchanges/bitstamp/bitstamp_wrapper.go +++ b/exchanges/bitstamp/bitstamp_wrapper.go @@ -373,8 +373,8 @@ func (b *Bitstamp) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) { return submitOrderResponse, err } - buy := s.OrderSide == order.Buy - market := s.OrderType == order.Market + buy := s.Side == order.Buy + market := s.Type == order.Market response, err := b.PlaceOrder(s.Pair.String(), s.Price, s.Amount, @@ -388,7 +388,7 @@ func (b *Bitstamp) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) { } submitOrderResponse.IsOrderPlaced = true - if s.OrderType == order.Market { + if s.Type == order.Market { submitOrderResponse.FullyMatched = true } return submitOrderResponse, nil @@ -402,7 +402,7 @@ func (b *Bitstamp) ModifyOrder(action *order.Modify) (string, error) { // CancelOrder cancels an order by its corresponding ID number func (b *Bitstamp) CancelOrder(order *order.Cancel) error { - orderIDInt, err := strconv.ParseInt(order.OrderID, 10, 64) + orderIDInt, err := strconv.ParseInt(order.ID, 10, 64) if err != nil { return err } @@ -534,10 +534,10 @@ func (b *Bitstamp) GetWebsocket() (*wshandler.Websocket, error) { // GetActiveOrders retrieves any orders that are active/open func (b *Bitstamp) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, error) { var currPair string - if len(req.Currencies) != 1 { + if len(req.Pairs) != 1 { currPair = "all" } else { - currPair = req.Currencies[0].String() + currPair = req.Pairs[0].String() } resp, err := b.GetOpenOrders(currPair) @@ -559,19 +559,19 @@ func (b *Bitstamp) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, } orders = append(orders, order.Detail{ - Amount: resp[i].Amount, - ID: strconv.FormatInt(resp[i].ID, 10), - Price: resp[i].Price, - OrderType: order.Limit, - OrderSide: orderSide, - OrderDate: tm, - CurrencyPair: currency.NewPairFromString(resp[i].Currency), - Exchange: b.Name, + Amount: resp[i].Amount, + ID: strconv.FormatInt(resp[i].ID, 10), + Price: resp[i].Price, + Type: order.Limit, + Side: orderSide, + Date: tm, + Pair: currency.NewPairFromString(resp[i].Currency), + Exchange: b.Name, }) } order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) - order.FilterOrdersByCurrencies(&orders, req.Currencies) + order.FilterOrdersByCurrencies(&orders, req.Pairs) return orders, nil } @@ -579,8 +579,8 @@ func (b *Bitstamp) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, // Can Limit response to specific order status func (b *Bitstamp) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail, error) { var currPair string - if len(req.Currencies) == 1 { - currPair = req.Currencies[0].String() + if len(req.Pairs) == 1 { + currPair = req.Pairs[0].String() } resp, err := b.GetUserTransactions(currPair) if err != nil { @@ -601,7 +601,7 @@ func (b *Bitstamp) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail, baseCurrency = currency.XRP default: log.Warnf(log.ExchangeSys, - "%s No base currency found for OrderID '%d'\n", + "%s No base currency found for ID '%d'\n", b.Name, resp[i].OrderID) } @@ -632,15 +632,15 @@ func (b *Bitstamp) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail, } orders = append(orders, order.Detail{ - ID: strconv.FormatInt(resp[i].OrderID, 10), - OrderDate: tm, - Exchange: b.Name, - CurrencyPair: currPair, + ID: strconv.FormatInt(resp[i].OrderID, 10), + Date: tm, + Exchange: b.Name, + Pair: currPair, }) } order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) - order.FilterOrdersByCurrencies(&orders, req.Currencies) + order.FilterOrdersByCurrencies(&orders, req.Pairs) return orders, nil } diff --git a/exchanges/bittrex/bittrex_test.go b/exchanges/bittrex/bittrex_test.go index 68312263..0eea30a0 100644 --- a/exchanges/bittrex/bittrex_test.go +++ b/exchanges/bittrex/bittrex_test.go @@ -344,11 +344,11 @@ func TestFormatWithdrawPermissions(t *testing.T) { func TestGetActiveOrders(t *testing.T) { var getOrdersRequest = order.GetOrdersRequest{ - OrderType: order.AnyType, - Currencies: []currency.Pair{currency.NewPairFromString(currPair)}, + Type: order.AnyType, + Pairs: []currency.Pair{currency.NewPairFromString(currPair)}, } - getOrdersRequest.Currencies[0].Delimiter = "-" + getOrdersRequest.Pairs[0].Delimiter = "-" _, err := b.GetActiveOrders(&getOrdersRequest) if areTestAPIKeysSet() && err != nil { @@ -360,7 +360,7 @@ func TestGetActiveOrders(t *testing.T) { func TestGetOrderHistory(t *testing.T) { var getOrdersRequest = order.GetOrdersRequest{ - OrderType: order.AnyType, + Type: order.AnyType, } _, err := b.GetOrderHistory(&getOrdersRequest) @@ -388,11 +388,11 @@ func TestSubmitOrder(t *testing.T) { Base: currency.BTC, Quote: currency.LTC, }, - OrderSide: order.Buy, - OrderType: order.Limit, - Price: 1, - Amount: 1, - ClientID: "meowOrder", + Side: order.Buy, + Type: order.Limit, + Price: 1, + Amount: 1, + ClientID: "meowOrder", } response, err := b.SubmitOrder(orderSubmission) if areTestAPIKeysSet() && (err != nil || !response.IsOrderPlaced) { @@ -409,10 +409,10 @@ func TestCancelExchangeOrder(t *testing.T) { currencyPair := currency.NewPair(currency.LTC, currency.BTC) var orderCancellation = &order.Cancel{ - OrderID: "1", + ID: "1", WalletAddress: core.BitcoinDonationAddress, AccountID: "1", - CurrencyPair: currencyPair, + Pair: currencyPair, } err := b.CancelOrder(orderCancellation) @@ -431,10 +431,10 @@ func TestCancelAllExchangeOrders(t *testing.T) { currencyPair := currency.NewPair(currency.LTC, currency.BTC) var orderCancellation = &order.Cancel{ - OrderID: "1", + ID: "1", WalletAddress: core.BitcoinDonationAddress, AccountID: "1", - CurrencyPair: currencyPair, + Pair: currencyPair, } resp, err := b.CancelAllOrders(orderCancellation) diff --git a/exchanges/bittrex/bittrex_wrapper.go b/exchanges/bittrex/bittrex_wrapper.go index 82f57eaa..4849091e 100644 --- a/exchanges/bittrex/bittrex_wrapper.go +++ b/exchanges/bittrex/bittrex_wrapper.go @@ -349,8 +349,8 @@ func (b *Bittrex) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) { return submitOrderResponse, err } - buy := s.OrderSide == order.Buy - if s.OrderType != order.Limit { + buy := s.Side == order.Buy + if s.Type != order.Limit { return submitOrderResponse, errors.New("limit orders only supported on exchange") } @@ -386,7 +386,7 @@ func (b *Bittrex) ModifyOrder(action *order.Modify) (string, error) { // CancelOrder cancels an order by its corresponding ID number func (b *Bittrex) CancelOrder(order *order.Cancel) error { - _, err := b.CancelExistingOrder(order.OrderID) + _, err := b.CancelExistingOrder(order.ID) return err } @@ -468,8 +468,8 @@ func (b *Bittrex) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) // GetActiveOrders retrieves any orders that are active/open func (b *Bittrex) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, error) { var currPair string - if len(req.Currencies) == 1 { - currPair = req.Currencies[0].String() + if len(req.Pairs) == 1 { + currPair = req.Pairs[0].String() } resp, err := b.GetOpenOrders(currPair) @@ -497,17 +497,17 @@ func (b *Bittrex) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, Amount: resp.Result[i].Quantity, RemainingAmount: resp.Result[i].QuantityRemaining, Price: resp.Result[i].Price, - OrderDate: orderDate, + Date: orderDate, ID: resp.Result[i].OrderUUID, Exchange: b.Name, - OrderType: orderType, - CurrencyPair: pair, + Type: orderType, + Pair: pair, }) } - order.FilterOrdersByType(&orders, req.OrderType) + order.FilterOrdersByType(&orders, req.Type) order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) - order.FilterOrdersByCurrencies(&orders, req.Currencies) + order.FilterOrdersByCurrencies(&orders, req.Pairs) return orders, nil } @@ -515,8 +515,8 @@ func (b *Bittrex) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, // Can Limit response to specific order status func (b *Bittrex) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail, error) { var currPair string - if len(req.Currencies) == 1 { - currPair = req.Currencies[0].String() + if len(req.Pairs) == 1 { + currPair = req.Pairs[0].String() } resp, err := b.GetOrderHistoryForCurrency(currPair) @@ -544,18 +544,18 @@ func (b *Bittrex) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail, Amount: resp.Result[i].Quantity, RemainingAmount: resp.Result[i].QuantityRemaining, Price: resp.Result[i].Price, - OrderDate: orderDate, + Date: orderDate, ID: resp.Result[i].OrderUUID, Exchange: b.Name, - OrderType: orderType, + Type: orderType, Fee: resp.Result[i].Commission, - CurrencyPair: pair, + Pair: pair, }) } - order.FilterOrdersByType(&orders, req.OrderType) + order.FilterOrdersByType(&orders, req.Type) order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) - order.FilterOrdersByCurrencies(&orders, req.Currencies) + order.FilterOrdersByCurrencies(&orders, req.Pairs) return orders, nil } diff --git a/exchanges/btcmarkets/btcmarkets.go b/exchanges/btcmarkets/btcmarkets.go index de9485df..0c867daf 100644 --- a/exchanges/btcmarkets/btcmarkets.go +++ b/exchanges/btcmarkets/btcmarkets.go @@ -70,7 +70,7 @@ const ( orderChange = "orderChange" heartbeat = "heartbeat" tick = "tick" - wsOB = "orderbook" + wsOB = "orderbookUpdate" trade = "trade" ) diff --git a/exchanges/btcmarkets/btcmarkets_test.go b/exchanges/btcmarkets/btcmarkets_test.go index 8f5d8d12..78d55b9d 100644 --- a/exchanges/btcmarkets/btcmarkets_test.go +++ b/exchanges/btcmarkets/btcmarkets_test.go @@ -10,6 +10,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/order" + "github.com/thrasher-corp/gocryptotrader/exchanges/sharedtestvalues" ) var b BTCMarkets @@ -45,6 +46,8 @@ func TestMain(m *testing.M) { if err != nil { log.Fatal(err) } + b.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride() + b.Websocket.TrafficAlert = sharedtestvalues.GetWebsocketStructChannelOverride() err = b.ValidateCredentials() if err != nil { @@ -446,12 +449,12 @@ func TestCancelBatchOrders(t *testing.T) { } } -func TestGetAccountInfo(t *testing.T) { +func TestFetchAccountInfo(t *testing.T) { t.Parallel() if !areTestAPIKeysSet() { t.Skip("API keys required but not set, skipping test") } - _, err := b.UpdateAccountInfo() + _, err := b.FetchAccountInfo() if err != nil { t.Error(err) } @@ -464,7 +467,7 @@ func TestGetOrderHistory(t *testing.T) { } _, err := b.GetOrderHistory(&order.GetOrdersRequest{ - OrderSide: order.Buy, + Side: order.Buy, }) if err != nil { t.Error(err) @@ -500,3 +503,215 @@ func TestGetActiveOrders(t *testing.T) { t.Fatal(err) } } + +func TestWsTicker(t *testing.T) { + pressXToJSON := []byte(`{ "marketId": "BTC-AUD", + "timestamp": "2019-04-08T18:56:17.405Z", + "bestBid": "7309.12", + "bestAsk": "7326.88", + "lastPrice": "7316.81", + "volume24h": "299.12936654", + "messageType": "tick" + }`) + err := b.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsTrade(t *testing.T) { + pressXToJSON := []byte(` { "marketId": "BTC-AUD", + "timestamp": "2019-04-08T20:54:27.632Z", + "tradeId": 3153171493, + "price": "7370.11", + "volume": "0.10901605", + "side": "Ask", + "messageType": "trade" + }`) + err := b.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsFundChange(t *testing.T) { + pressXToJSON := []byte(`{ + "fundtransferId": 276811, + "type": "Deposit", + "status": "Complete", + "timestamp": "2019-04-16T01:38:02.931Z", + "amount": "0.001", + "currency": "BTC", + "fee": "0", + "messageType": "fundChange" +}`) + err := b.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsOrderbookUpdate(t *testing.T) { + pressXToJSON := []byte(`{ "marketId": "LTC-AUD", + "snapshot": true, + "timestamp": "2020-01-08T19:47:13.986Z", + "snapshotId": 1578512833978000, + "bids": + [ [ "99.57", "0.55", 1 ], + [ "97.62", "3.20", 2 ], + [ "97.07", "0.9", 1 ], + [ "96.7", "1.9", 1 ], + [ "95.8", "7.0", 1 ] ], + "asks": + [ [ "100", "3.79", 3 ], + [ "101", "6.32", 2 ] ], + "messageType": "orderbookUpdate" + }`) + err := b.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } + + pressXToJSON = []byte(` { "marketId": "LTC-AUD", + "timestamp": "2020-01-08T19:47:24.054Z", + "snapshotId": 1578512844045000, + "bids": [ ["99.81", "1.2", 1 ], ["95.8", "0", 0 ]], + "asks": [ ["100", "3.2", 2 ] ], + "messageType": "orderbookUpdate" + }`) + err = b.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsHeartbeats(t *testing.T) { + pressXToJSON := []byte(`{ + "messageType": "error", + "code": 3, + "message": "invalid channel names" +}`) + err := b.wsHandleData(pressXToJSON) + if err == nil { + t.Error("expected error") + } + + pressXToJSON = []byte(`{ +"messageType": "error", +"code": 3, +"message": "invalid marketIds" +}`) + err = b.wsHandleData(pressXToJSON) + if err == nil { + t.Error("expected error") + } + + pressXToJSON = []byte(`{ +"messageType": "error", +"code": 1, +"message": "authentication failed. invalid key" +}`) + err = b.wsHandleData(pressXToJSON) + if err == nil { + t.Error("expected error") + } +} + +func TestWsOrders(t *testing.T) { + pressXToJSON := []byte(`{ + "orderId": 79003, + "marketId": "BTC-AUD", + "side": "Bid", + "type": "Limit", + "openVolume": "1", + "status": "Placed", + "triggerStatus": "", + "trades": [], + "timestamp": "2019-04-08T20:41:19.339Z", + "messageType": "orderChange" + }`) + err := b.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } + + pressXToJSON = []byte(` { + "orderId": 79033, + "marketId": "BTC-AUD", + "side": "Bid", + "type": "Limit", + "openVolume": "0", + "status": "Fully Matched", + "triggerStatus": "", + "trades": [{ + "tradeId":31727, + "price":"0.1634", + "volume":"10", + "fee":"0.001", + "liquidityType":"Taker" + }], + "timestamp": "2019-04-08T20:50:39.658Z", + "messageType": "orderChange" + }`) + err = b.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } + + pressXToJSON = []byte(` { + "orderId": 79003, + "marketId": "BTC-AUD", + "side": "Bid", + "type": "Limit", + "openVolume": "1", + "status": "Cancelled", + "triggerStatus": "", + "trades": [], + "timestamp": "2019-04-08T20:41:41.857Z", + "messageType": "orderChange" + }`) + err = b.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } + + pressXToJSON = []byte(` { + "orderId": 79003, + "marketId": "BTC-AUD", + "side": "Bid", + "type": "Limit", + "openVolume": "1", + "status": "Partially Matched", + "triggerStatus": "", + "trades": [{ + "tradeId":31927, + "price":"0.1634", + "volume":"5", + "fee":"0.001", + "liquidityType":"Taker" + }], + "timestamp": "2019-04-08T20:41:41.857Z", + "messageType": "orderChange" + }`) + err = b.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } + + pressXToJSON = []byte(` { + "orderId": 7903, + "marketId": "BTC-AUD", + "side": "Bid", + "type": "Limit", + "openVolume": "1.2", + "status": "Placed", + "triggerStatus": "Triggered", + "trades": [], + "timestamp": "2019-04-08T20:41:41.857Z", + "messageType": "orderChange" + }`) + err = b.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} diff --git a/exchanges/btcmarkets/btcmarkets_types.go b/exchanges/btcmarkets/btcmarkets_types.go index 64730e9a..6fdb5fc0 100644 --- a/exchanges/btcmarkets/btcmarkets_types.go +++ b/exchanges/btcmarkets/btcmarkets_types.go @@ -379,11 +379,12 @@ type WsTrade struct { // WsOrderbook message received for orderbook data type WsOrderbook struct { - Currency string `json:"marketId"` - Timestamp time.Time `json:"timestamp"` - Bids [][]string `json:"bids"` - Asks [][]string `json:"asks"` - MessageType string `json:"messageType"` + Currency string `json:"marketId"` + Timestamp time.Time `json:"timestamp"` + Bids [][]interface{} `json:"bids"` + Asks [][]interface{} `json:"asks"` + MessageType string `json:"messageType"` + Snapshot bool `json:"snapshot"` } // WsFundTransfer stores fund transfer data for websocket diff --git a/exchanges/btcmarkets/btcmarkets_websocket.go b/exchanges/btcmarkets/btcmarkets_websocket.go index 142b5526..75d7352e 100644 --- a/exchanges/btcmarkets/btcmarkets_websocket.go +++ b/exchanges/btcmarkets/btcmarkets_websocket.go @@ -18,6 +18,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" + "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wsorderbook" "github.com/thrasher-corp/gocryptotrader/log" ) @@ -38,20 +39,16 @@ func (b *BTCMarkets) WsConnect() error { if b.Verbose { log.Debugf(log.ExchangeSys, "%s Connected to Websocket.\n", b.Name) } - go b.WsHandleData() + go b.wsReadData() if b.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) { b.createChannels() - if err != nil { - b.Websocket.DataHandler <- err - b.Websocket.SetCanUseAuthenticatedEndpoints(false) - } } b.generateDefaultSubscriptions() return nil } -// WsHandleData handles websocket data from WsReadData -func (b *BTCMarkets) WsHandleData() { +// wsReadData receives and passes on websocket messages for processing +func (b *BTCMarkets) wsReadData() { b.Websocket.Wg.Add(1) defer func() { b.Websocket.Wg.Done() @@ -68,149 +65,223 @@ func (b *BTCMarkets) WsHandleData() { return } b.Websocket.TrafficAlert <- struct{}{} - var wsResponse WsMessageType - err = json.Unmarshal(resp.Raw, &wsResponse) + err = b.wsHandleData(resp.Raw) if err != nil { b.Websocket.DataHandler <- err - continue - } - switch wsResponse.MessageType { - case heartbeat: - if b.Verbose { - log.Debugf(log.ExchangeSys, "%v - Websocket heartbeat received %s", b.Name, resp.Raw) - } - case wsOB: - var ob WsOrderbook - err := json.Unmarshal(resp.Raw, &ob) - if err != nil { - b.Websocket.DataHandler <- err - continue - } - - p := currency.NewPairFromString(ob.Currency) - var bids, asks []orderbook.Item - for x := range ob.Bids { - var price, amount float64 - price, err = strconv.ParseFloat(ob.Bids[x][0], 64) - if err != nil { - b.Websocket.DataHandler <- err - continue - } - amount, err = strconv.ParseFloat(ob.Bids[x][1], 64) - if err != nil { - b.Websocket.DataHandler <- err - continue - } - bids = append(bids, orderbook.Item{ - Amount: amount, - Price: price, - }) - } - for x := range ob.Asks { - var price, amount float64 - price, err = strconv.ParseFloat(ob.Asks[x][0], 64) - if err != nil { - b.Websocket.DataHandler <- err - continue - } - amount, err = strconv.ParseFloat(ob.Asks[x][1], 64) - if err != nil { - b.Websocket.DataHandler <- err - continue - } - asks = append(asks, orderbook.Item{ - Amount: amount, - Price: price, - }) - } - err = b.Websocket.Orderbook.LoadSnapshot(&orderbook.Base{ - Pair: p, - Bids: bids, - Asks: asks, - LastUpdated: ob.Timestamp, - AssetType: asset.Spot, - ExchangeName: b.Name, - }) - if err != nil { - b.Websocket.DataHandler <- err - continue - } - b.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{ - Pair: p, - Asset: asset.Spot, - Exchange: b.Name, - } - case trade: - var trade WsTrade - err := json.Unmarshal(resp.Raw, &trade) - if err != nil { - b.Websocket.DataHandler <- err - continue - } - p := currency.NewPairFromString(trade.Currency) - b.Websocket.DataHandler <- wshandler.TradeData{ - Timestamp: trade.Timestamp, - CurrencyPair: p, - AssetType: asset.Spot, - Exchange: b.Name, - Price: trade.Price, - Amount: trade.Volume, - Side: order.SideUnknown.String(), - EventType: order.Unknown.String(), - } - case tick: - var tick WsTick - err := json.Unmarshal(resp.Raw, &tick) - if err != nil { - b.Websocket.DataHandler <- err - continue - } - - p := currency.NewPairFromString(tick.Currency) - - b.Websocket.DataHandler <- &ticker.Price{ - ExchangeName: b.Name, - Volume: tick.Volume, - High: tick.High24, - Low: tick.Low24h, - Bid: tick.Bid, - Ask: tick.Ask, - Last: tick.Last, - LastUpdated: tick.Timestamp, - AssetType: asset.Spot, - Pair: p, - } - case fundChange: - var transferData WsFundTransfer - err := json.Unmarshal(resp.Raw, &transferData) - if err != nil { - b.Websocket.DataHandler <- err - continue - } - b.Websocket.DataHandler <- transferData - case orderChange: - var orderData WsOrderChange - err := json.Unmarshal(resp.Raw, &orderData) - if err != nil { - b.Websocket.DataHandler <- err - continue - } - b.Websocket.DataHandler <- orderData - case "error": - var wsErr WsError - err := json.Unmarshal(resp.Raw, &wsErr) - if err != nil { - b.Websocket.DataHandler <- err - continue - } - b.Websocket.DataHandler <- fmt.Errorf("%v websocket error. Code: %v Message: %v", b.Name, wsErr.Code, wsErr.Message) - default: - b.Websocket.DataHandler <- fmt.Errorf("%v Unhandled websocket message %s", b.Name, resp.Raw) } } } } +func (b *BTCMarkets) wsHandleData(respRaw []byte) error { + var wsResponse WsMessageType + err := json.Unmarshal(respRaw, &wsResponse) + if err != nil { + return err + } + switch wsResponse.MessageType { + case heartbeat: + if b.Verbose { + log.Debugf(log.ExchangeSys, "%v - Websocket heartbeat received %s", b.Name, respRaw) + } + case wsOB: + var ob WsOrderbook + err := json.Unmarshal(respRaw, &ob) + if err != nil { + return err + } + + p := currency.NewPairFromString(ob.Currency) + var bids, asks []orderbook.Item + for x := range ob.Bids { + var price, amount float64 + price, err = strconv.ParseFloat(ob.Bids[x][0].(string), 64) + if err != nil { + return err + } + amount, err = strconv.ParseFloat(ob.Bids[x][1].(string), 64) + if err != nil { + return err + } + bids = append(bids, orderbook.Item{ + Amount: amount, + Price: price, + OrderCount: int64(ob.Bids[x][2].(float64)), + }) + } + for x := range ob.Asks { + var price, amount float64 + price, err = strconv.ParseFloat(ob.Asks[x][0].(string), 64) + if err != nil { + return err + } + amount, err = strconv.ParseFloat(ob.Asks[x][1].(string), 64) + if err != nil { + return err + } + asks = append(asks, orderbook.Item{ + Amount: amount, + Price: price, + OrderCount: int64(ob.Asks[x][2].(float64)), + }) + } + if ob.Snapshot { + err = b.Websocket.Orderbook.LoadSnapshot(&orderbook.Base{ + Pair: p, + Bids: bids, + Asks: asks, + LastUpdated: ob.Timestamp, + AssetType: asset.Spot, + ExchangeName: b.Name, + }) + } else { + err = b.Websocket.Orderbook.Update(&wsorderbook.WebsocketOrderbookUpdate{ + UpdateTime: ob.Timestamp, + Asset: asset.Spot, + Bids: bids, + Asks: asks, + Pair: p, + }) + } + + if err != nil { + return err + } + b.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{ + Pair: p, + Asset: asset.Spot, + Exchange: b.Name, + } + case trade: + var trade WsTrade + err := json.Unmarshal(respRaw, &trade) + if err != nil { + return err + } + p := currency.NewPairFromString(trade.Currency) + b.Websocket.DataHandler <- wshandler.TradeData{ + Timestamp: trade.Timestamp, + CurrencyPair: p, + AssetType: asset.Spot, + Exchange: b.Name, + Price: trade.Price, + Amount: trade.Volume, + Side: order.UnknownSide, + EventType: order.UnknownType, + } + case tick: + var tick WsTick + err := json.Unmarshal(respRaw, &tick) + if err != nil { + return err + } + + p := currency.NewPairFromString(tick.Currency) + + b.Websocket.DataHandler <- &ticker.Price{ + ExchangeName: b.Name, + Volume: tick.Volume, + High: tick.High24, + Low: tick.Low24h, + Bid: tick.Bid, + Ask: tick.Ask, + Last: tick.Last, + LastUpdated: tick.Timestamp, + AssetType: asset.Spot, + Pair: p, + } + case fundChange: + var transferData WsFundTransfer + err := json.Unmarshal(respRaw, &transferData) + if err != nil { + return err + } + b.Websocket.DataHandler <- transferData + case orderChange: + var orderData WsOrderChange + err := json.Unmarshal(respRaw, &orderData) + if err != nil { + return err + } + originalAmount := orderData.OpenVolume + var price float64 + var trades []order.TradeHistory + var orderID = strconv.FormatInt(orderData.OrderID, 10) + for x := range orderData.Trades { + var isMaker bool + if orderData.Trades[x].LiquidityType == "Maker" { + isMaker = true + } + trades = append(trades, order.TradeHistory{ + Price: orderData.Trades[x].Price, + Amount: orderData.Trades[x].Volume, + Fee: orderData.Trades[x].Fee, + Exchange: b.Name, + TID: strconv.FormatInt(orderData.Trades[x].TradeID, 10), + IsMaker: isMaker, + }) + price = orderData.Trades[x].Price + originalAmount += orderData.Trades[x].Volume + } + oType, err := order.StringToOrderType(orderData.OrderType) + if err != nil { + b.Websocket.DataHandler <- order.ClassificationError{ + Exchange: b.Name, + OrderID: orderID, + Err: err, + } + } + oSide, err := order.StringToOrderSide(orderData.Side) + if err != nil { + b.Websocket.DataHandler <- order.ClassificationError{ + Exchange: b.Name, + OrderID: orderID, + Err: err, + } + } + oStatus, err := order.StringToOrderStatus(orderData.Status) + if err != nil { + b.Websocket.DataHandler <- order.ClassificationError{ + Exchange: b.Name, + OrderID: orderID, + Err: err, + } + } + p := currency.NewPairFromString(orderData.MarketID) + var a asset.Item + a, err = b.GetPairAssetType(p) + if err != nil { + return err + } + b.Websocket.DataHandler <- &order.Detail{ + Price: price, + Amount: originalAmount, + RemainingAmount: orderData.OpenVolume, + Exchange: b.Name, + ID: orderID, + ClientID: b.API.Credentials.ClientID, + Type: oType, + Side: oSide, + Status: oStatus, + AssetType: a, + Date: orderData.Timestamp, + Trades: trades, + Pair: p, + } + case "error": + var wsErr WsError + err := json.Unmarshal(respRaw, &wsErr) + if err != nil { + return err + } + return fmt.Errorf("%v websocket error. Code: %v Message: %v", b.Name, wsErr.Code, wsErr.Message) + default: + b.Websocket.DataHandler <- wshandler.UnhandledMessageWarning{Message: b.Name + wshandler.UnhandledMessage + string(respRaw)} + return nil + } + return nil +} + func (b *BTCMarkets) generateDefaultSubscriptions() { var channels = []string{tick, trade, wsOB} enabledCurrencies := b.GetEnabledPairs(asset.Spot) diff --git a/exchanges/btcmarkets/btcmarkets_wrapper.go b/exchanges/btcmarkets/btcmarkets_wrapper.go index 37cf4b65..50188ab3 100644 --- a/exchanges/btcmarkets/btcmarkets_wrapper.go +++ b/exchanges/btcmarkets/btcmarkets_wrapper.go @@ -101,6 +101,8 @@ func (b *BTCMarkets) SetDefaults() { AccountInfo: true, Subscribe: true, AuthenticatedEndpoints: true, + GetOrders: true, + GetOrder: true, }, WithdrawPermissions: exchange.AutoWithdrawCrypto | exchange.AutoWithdrawFiat, @@ -159,6 +161,14 @@ func (b *BTCMarkets) Setup(exch *config.ExchangeConfig) error { ResponseMaxLimit: exch.WebsocketResponseMaxLimit, } + b.Websocket.Orderbook.Setup( + exch.WebsocketOrderbookBufferLimit, + true, + true, + false, + false, + exch.Name) + return nil } @@ -370,18 +380,18 @@ func (b *BTCMarkets) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) return resp, err } - if s.OrderSide == order.Sell { - s.OrderSide = order.Ask + if s.Side == order.Sell { + s.Side = order.Ask } - if s.OrderSide == order.Buy { - s.OrderSide = order.Bid + if s.Side == order.Buy { + s.Side = order.Bid } tempResp, err := b.NewOrder(b.FormatExchangeCurrency(s.Pair, asset.Spot).String(), s.Price, s.Amount, - s.OrderType.String(), - s.OrderSide.String(), + s.Type.String(), + s.Side.String(), s.TriggerPrice, s.TargetAmount, "", @@ -404,7 +414,7 @@ func (b *BTCMarkets) ModifyOrder(action *order.Modify) (string, error) { // CancelOrder cancels an order by its corresponding ID number func (b *BTCMarkets) CancelOrder(o *order.Cancel) error { - _, err := b.RemoveOrder(o.OrderID) + _, err := b.RemoveOrder(o.ID) return err } @@ -446,27 +456,27 @@ func (b *BTCMarkets) GetOrderInfo(orderID string) (order.Detail, error) { } resp.Exchange = b.Name resp.ID = orderID - resp.CurrencyPair = currency.NewPairFromString(o.MarketID) + resp.Pair = currency.NewPairFromString(o.MarketID) resp.Price = o.Price - resp.OrderDate = o.CreationTime + resp.Date = o.CreationTime resp.ExecutedAmount = o.Amount - o.OpenAmount - resp.OrderSide = order.Bid + resp.Side = order.Bid if o.Side == ask { - resp.OrderSide = order.Ask + resp.Side = order.Ask } switch o.Type { case limit: - resp.OrderType = order.Limit + resp.Type = order.Limit case market: - resp.OrderType = order.Market + resp.Type = order.Market case stopLimit: - resp.OrderType = order.Stop + resp.Type = order.Stop case stop: - resp.OrderType = order.Stop + resp.Type = order.Stop case takeProfit: - resp.OrderType = order.ImmediateOrCancel + resp.Type = order.ImmediateOrCancel default: - resp.OrderType = order.Unknown + resp.Type = order.UnknownType } resp.RemainingAmount = o.OpenAmount switch o.Status { @@ -561,42 +571,42 @@ func (b *BTCMarkets) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, err // GetActiveOrders retrieves any orders that are active/open func (b *BTCMarkets) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, error) { - if len(req.Currencies) == 0 { + if len(req.Pairs) == 0 { allPairs := b.GetEnabledPairs(asset.Spot) for a := range allPairs { - req.Currencies = append(req.Currencies, + req.Pairs = append(req.Pairs, allPairs[a]) } } var resp []order.Detail - for x := range req.Currencies { - tempData, err := b.GetOrders(b.FormatExchangeCurrency(req.Currencies[x], asset.Spot).String(), -1, -1, -1, true) + for x := range req.Pairs { + tempData, err := b.GetOrders(b.FormatExchangeCurrency(req.Pairs[x], asset.Spot).String(), -1, -1, -1, true) if err != nil { return resp, err } for y := range tempData { var tempResp order.Detail tempResp.Exchange = b.Name - tempResp.CurrencyPair = req.Currencies[x] + tempResp.Pair = req.Pairs[x] tempResp.ID = tempData[y].OrderID - tempResp.OrderSide = order.Bid + tempResp.Side = order.Bid if tempData[y].Side == ask { - tempResp.OrderSide = order.Ask + tempResp.Side = order.Ask } - tempResp.OrderDate = tempData[y].CreationTime + tempResp.Date = tempData[y].CreationTime switch tempData[y].Type { case limit: - tempResp.OrderType = order.Limit + tempResp.Type = order.Limit case market: - tempResp.OrderType = order.Market + tempResp.Type = order.Market default: log.Errorf(log.ExchangeSys, "%s unknown order type %s getting order", b.Name, tempData[y].Type) - tempResp.OrderType = order.Unknown + tempResp.Type = order.UnknownType } switch tempData[y].Status { case orderAccepted: @@ -620,9 +630,9 @@ func (b *BTCMarkets) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detai resp = append(resp, tempResp) } } - order.FilterOrdersByType(&resp, req.OrderType) + order.FilterOrdersByType(&resp, req.Type) order.FilterOrdersByTickRange(&resp, req.StartTicks, req.EndTicks) - order.FilterOrdersBySide(&resp, req.OrderSide) + order.FilterOrdersBySide(&resp, req.Side) return resp, nil } @@ -632,7 +642,7 @@ func (b *BTCMarkets) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detai var resp []order.Detail var tempResp order.Detail var tempArray []string - if len(req.Currencies) == 0 { + if len(req.Pairs) == 0 { orders, err := b.GetOrders("", -1, -1, -1, false) if err != nil { return resp, err @@ -641,8 +651,8 @@ func (b *BTCMarkets) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detai tempArray = append(tempArray, orders[x].OrderID) } } - for y := range req.Currencies { - orders, err := b.GetOrders(b.FormatExchangeCurrency(req.Currencies[y], asset.Spot).String(), -1, -1, -1, false) + for y := range req.Pairs { + orders, err := b.GetOrders(b.FormatExchangeCurrency(req.Pairs[y], asset.Spot).String(), -1, -1, -1, false) if err != nil { return resp, err } @@ -674,13 +684,13 @@ func (b *BTCMarkets) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detai continue } tempResp.Exchange = b.Name - tempResp.CurrencyPair = currency.NewPairFromString(tempData.Orders[c].MarketID) - tempResp.OrderSide = order.Bid + tempResp.Pair = currency.NewPairFromString(tempData.Orders[c].MarketID) + tempResp.Side = order.Bid if tempData.Orders[c].Side == ask { - tempResp.OrderSide = order.Ask + tempResp.Side = order.Ask } tempResp.ID = tempData.Orders[c].OrderID - tempResp.OrderDate = tempData.Orders[c].CreationTime + tempResp.Date = tempData.Orders[c].CreationTime tempResp.Price = tempData.Orders[c].Price tempResp.ExecutedAmount = tempData.Orders[c].Amount resp = append(resp, tempResp) diff --git a/exchanges/btse/btse_test.go b/exchanges/btse/btse_test.go index 633a6223..ab190711 100644 --- a/exchanges/btse/btse_test.go +++ b/exchanges/btse/btse_test.go @@ -11,6 +11,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" "github.com/thrasher-corp/gocryptotrader/exchanges/order" + "github.com/thrasher-corp/gocryptotrader/exchanges/sharedtestvalues" ) // Please supply your own keys here to do better tests @@ -43,7 +44,8 @@ func TestMain(m *testing.M) { if err != nil { log.Fatal(err) } - + b.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride() + b.Websocket.TrafficAlert = sharedtestvalues.GetWebsocketStructChannelOverride() os.Exit(m.Run()) } @@ -163,7 +165,7 @@ func TestGetActiveOrders(t *testing.T) { t.Skip("API keys not set, skipping test") } var getOrdersRequest = order.GetOrdersRequest{ - OrderType: order.AnyType, + Type: order.AnyType, } _, err := b.GetActiveOrders(&getOrdersRequest) @@ -178,7 +180,7 @@ func TestGetOrderHistory(t *testing.T) { t.Skip("API keys not set, skipping test") } var getOrdersRequest = order.GetOrdersRequest{ - OrderType: order.AnyType, + Type: order.AnyType, } _, err := b.GetOrderHistory(&getOrdersRequest) if err != nil { @@ -299,11 +301,11 @@ func TestSubmitOrder(t *testing.T) { Base: currency.BTC, Quote: currency.USD, }, - OrderSide: order.Buy, - OrderType: order.Limit, - Price: 100000, - Amount: 0.1, - ClientID: "meowOrder", + Side: order.Buy, + Type: order.Limit, + Price: 100000, + Amount: 0.1, + ClientID: "meowOrder", } response, err := b.SubmitOrder(orderSubmission) if areTestAPIKeysSet() && (err != nil || !response.IsOrderPlaced) { @@ -322,10 +324,10 @@ func TestCancelExchangeOrder(t *testing.T) { currency.USD.String(), "-") var orderCancellation = &order.Cancel{ - OrderID: "b334ecef-2b42-4998-b8a4-b6b14f6d2671", + ID: "b334ecef-2b42-4998-b8a4-b6b14f6d2671", WalletAddress: core.BitcoinDonationAddress, AccountID: "1", - CurrencyPair: currencyPair, + Pair: currencyPair, } err := b.CancelOrder(orderCancellation) if err != nil { @@ -342,10 +344,10 @@ func TestCancelAllExchangeOrders(t *testing.T) { currency.USD.String(), "-") var orderCancellation = &order.Cancel{ - OrderID: "1", + ID: "1", WalletAddress: core.BitcoinDonationAddress, AccountID: "1", - CurrencyPair: currencyPair, + Pair: currencyPair, } resp, err := b.CancelAllOrders(orderCancellation) @@ -358,3 +360,54 @@ func TestCancelAllExchangeOrders(t *testing.T) { } } } + +func TestWsOrderbook(t *testing.T) { + pressXToJSON := []byte(`{"topic":"orderBookApi:BTC-USD_0","data":{"buyQuote":[{"price":"9272.0","size":"0.077"},{"price":"9271.0","size":"1.122"},{"price":"9270.0","size":"2.548"},{"price":"9267.5","size":"1.015"},{"price":"9265.5","size":"0.930"},{"price":"9265.0","size":"0.475"},{"price":"9264.5","size":"2.216"},{"price":"9264.0","size":"9.709"},{"price":"9263.5","size":"3.667"},{"price":"9263.0","size":"8.481"},{"price":"9262.5","size":"7.660"},{"price":"9262.0","size":"9.689"},{"price":"9261.5","size":"4.213"},{"price":"9261.0","size":"1.491"},{"price":"9260.5","size":"6.264"},{"price":"9260.0","size":"1.690"},{"price":"9259.5","size":"5.718"},{"price":"9259.0","size":"2.706"},{"price":"9258.5","size":"0.192"},{"price":"9258.0","size":"1.592"},{"price":"9257.5","size":"1.749"},{"price":"9257.0","size":"8.104"},{"price":"9256.0","size":"0.161"},{"price":"9252.0","size":"1.544"},{"price":"9249.5","size":"1.462"},{"price":"9247.5","size":"1.833"},{"price":"9247.0","size":"0.168"},{"price":"9245.5","size":"1.941"},{"price":"9244.0","size":"1.423"},{"price":"9243.5","size":"0.175"}],"currency":"USD","sellQuote":[{"price":"9303.5","size":"1.839"},{"price":"9303.0","size":"2.067"},{"price":"9302.0","size":"0.117"},{"price":"9298.5","size":"1.569"},{"price":"9297.0","size":"1.527"},{"price":"9295.0","size":"0.184"},{"price":"9294.0","size":"1.785"},{"price":"9289.0","size":"1.673"},{"price":"9287.5","size":"4.194"},{"price":"9287.0","size":"6.622"},{"price":"9286.5","size":"2.147"},{"price":"9286.0","size":"3.348"},{"price":"9285.5","size":"5.655"},{"price":"9285.0","size":"10.423"},{"price":"9284.5","size":"6.233"},{"price":"9284.0","size":"8.860"},{"price":"9283.5","size":"9.441"},{"price":"9283.0","size":"3.455"},{"price":"9282.5","size":"11.033"},{"price":"9282.0","size":"11.471"},{"price":"9281.5","size":"4.742"},{"price":"9281.0","size":"14.789"},{"price":"9280.5","size":"11.117"},{"price":"9280.0","size":"0.807"},{"price":"9279.5","size":"1.651"},{"price":"9279.0","size":"0.244"},{"price":"9278.5","size":"0.533"},{"price":"9277.0","size":"1.447"},{"price":"9273.0","size":"1.976"},{"price":"9272.5","size":"0.093"}]}}`) + err := b.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsTrades(t *testing.T) { + pressXToJSON := []byte(`{"topic":"tradeHistory:BTC-USD","data":[{"amount":0.09,"gain":1,"newest":0,"price":9273.6,"serialId":0,"transactionUnixtime":1580349090693}]}`) + err := b.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsOrderNotification(t *testing.T) { + status := []string{"ORDER_INSERTED", "ORDER_CANCELLED", "TRIGGER_INSERTED", "ORDER_FULL_TRANSACTED", "ORDER_PARTIALLY_TRANSACTED", "INSUFFICIENT_BALANCE", "TRIGGER_ACTIVATED", "MARKET_UNAVAILABLE"} + for i := range status { + pressXToJSON := []byte(`{"topic": "notificationApi","data": [{"symbol": "BTC-USD","orderID": "1234","orderMode": "MODE_BUY","orderType": "TYPE_LIMIT","price": "1","size": "1","status": "` + status[i] + `","timestamp": "1580349090693","type": "STOP","triggerPrice": "1"}]}`) + err := b.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } + } +} + +func TestStatusToStandardStatus(t *testing.T) { + type TestCases struct { + Case string + Result order.Status + } + testCases := []TestCases{ + {Case: "ORDER_INSERTED", Result: order.New}, + {Case: "TRIGGER_INSERTED", Result: order.New}, + {Case: "ORDER_CANCELLED", Result: order.Cancelled}, + {Case: "ORDER_FULL_TRANSACTED", Result: order.Filled}, + {Case: "ORDER_PARTIALLY_TRANSACTED", Result: order.PartiallyFilled}, + {Case: "TRIGGER_ACTIVATED", Result: order.Active}, + {Case: "INSUFFICIENT_BALANCE", Result: order.InsufficientBalance}, + {Case: "MARKET_UNAVAILABLE", Result: order.MarketUnavailable}, + {Case: "LOL", Result: order.UnknownStatus}, + } + for i := range testCases { + result, _ := stringToOrderStatus(testCases[i].Case) + if result != testCases[i].Result { + t.Errorf("Exepcted: %v, received: %v", testCases[i].Result, result) + } + } +} diff --git a/exchanges/btse/btse_types.go b/exchanges/btse/btse_types.go index 75672552..e6d9b935 100644 --- a/exchanges/btse/btse_types.go +++ b/exchanges/btse/btse_types.go @@ -165,3 +165,23 @@ type wsTradeHistory struct { Topic string `json:"topic"` Data []wsTradeData `json:"data"` } + +type wsNotification struct { + Topic string `json:"topic"` + Data []wsOrderUpdate `json:"data"` +} + +type wsOrderUpdate struct { + OrderID string `json:"orderID"` + OrderMode string `json:"orderMode"` + OrderType string `json:"orderType"` + PegPriceDeviation string `json:"pegPriceDeviation"` + Price float64 `json:"price,string"` + Size float64 `json:"size,string"` + Status string `json:"status"` + Stealth string `json:"stealth"` + Symbol string `json:"symbol"` + Timestamp int64 `json:"timestamp,string"` + TriggerPrice float64 `json:"triggerPrice,string"` + Type string `json:"type"` +} diff --git a/exchanges/btse/btse_websocket.go b/exchanges/btse/btse_websocket.go index b8f26178..5402dd79 100644 --- a/exchanges/btse/btse_websocket.go +++ b/exchanges/btse/btse_websocket.go @@ -10,12 +10,13 @@ import ( "time" "github.com/gorilla/websocket" + "github.com/thrasher-corp/gocryptotrader/common/crypto" "github.com/thrasher-corp/gocryptotrader/currency" + exchange "github.com/thrasher-corp/gocryptotrader/exchanges" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" - "github.com/thrasher-corp/gocryptotrader/log" ) const ( @@ -37,14 +38,60 @@ func (b *BTSE) WsConnect() error { MessageType: websocket.PingMessage, Delay: btseWebsocketTimer, }) - go b.WsHandleData() - b.GenerateDefaultSubscriptions() + go b.wsReadData() + if b.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) { + err = b.WsAuthenticate() + if err != nil { + b.Websocket.DataHandler <- err + b.Websocket.SetCanUseAuthenticatedEndpoints(false) + } + } + + b.GenerateDefaultSubscriptions() return nil } -// WsHandleData handles read data from websocket connection -func (b *BTSE) WsHandleData() { +// WsAuthenticate Send an authentication message to receive auth data +func (b *BTSE) WsAuthenticate() error { + nonce := strconv.FormatInt(time.Now().UnixNano()/int64(time.Millisecond), 10) + path := "/spotWS" + nonce + hmac := crypto.GetHMAC( + crypto.HashSHA512_384, + []byte((path + nonce)), + []byte(b.API.Credentials.Secret), + ) + sign := crypto.HexEncodeToString(hmac) + req := wsSub{ + Operation: "authKeyExpires", + Arguments: []string{b.API.Credentials.Key, nonce, sign}, + } + return b.WebsocketConn.SendJSONMessage(req) +} + +func stringToOrderStatus(status string) (order.Status, error) { + switch status { + case "ORDER_INSERTED", "TRIGGER_INSERTED": + return order.New, nil + case "ORDER_CANCELLED": + return order.Cancelled, nil + case "ORDER_FULL_TRANSACTED": + return order.Filled, nil + case "ORDER_PARTIALLY_TRANSACTED": + return order.PartiallyFilled, nil + case "TRIGGER_ACTIVATED": + return order.Active, nil + case "INSUFFICIENT_BALANCE": + return order.InsufficientBalance, nil + case "MARKET_UNAVAILABLE": + return order.MarketUnavailable, nil + default: + return order.UnknownStatus, errors.New(status + " not recognised as order status") + } +} + +// wsReadData receives and passes on websocket messages for processing +func (b *BTSE) wsReadData() { b.Websocket.Wg.Add(1) defer func() { @@ -63,106 +110,177 @@ func (b *BTSE) WsHandleData() { return } b.Websocket.TrafficAlert <- struct{}{} - - type Result map[string]interface{} - result := Result{} - err = json.Unmarshal(resp.Raw, &result) + err = b.wsHandleData(resp.Raw) if err != nil { b.Websocket.DataHandler <- err - continue - } - switch { - case strings.Contains(result["topic"].(string), "tradeHistory"): - var tradeHistory wsTradeHistory - err = json.Unmarshal(resp.Raw, &tradeHistory) - if err != nil { - b.Websocket.DataHandler <- err - continue - } - for x := range tradeHistory.Data { - side := order.Buy.String() - if tradeHistory.Data[x].Gain == -1 { - side = order.Sell.String() - } - b.Websocket.DataHandler <- wshandler.TradeData{ - Timestamp: time.Unix(0, tradeHistory.Data[x].TransactionTime*int64(time.Millisecond)), - CurrencyPair: currency.NewPairFromString(strings.Replace(tradeHistory.Topic, "tradeHistory:", "", 1)), - AssetType: asset.Spot, - Exchange: b.Name, - Price: tradeHistory.Data[x].Price, - Amount: tradeHistory.Data[x].Amount, - Side: side, - } - } - case strings.Contains(result["topic"].(string), "orderBookApi"): - var t wsOrderBook - err = json.Unmarshal(resp.Raw, &t) - if err != nil { - b.Websocket.DataHandler <- err - continue - } - var newOB orderbook.Base - var price, amount float64 - for i := range t.Data.SellQuote { - p := strings.Replace(t.Data.SellQuote[i].Price, ",", "", -1) - price, err = strconv.ParseFloat(p, 64) - if err != nil { - b.Websocket.DataHandler <- err - continue - } - a := strings.Replace(t.Data.SellQuote[i].Size, ",", "", -1) - amount, err = strconv.ParseFloat(a, 64) - if err != nil { - b.Websocket.DataHandler <- err - continue - } - newOB.Asks = append(newOB.Asks, orderbook.Item{ - Price: price, - Amount: amount, - }) - } - for j := range t.Data.BuyQuote { - p := strings.Replace(t.Data.BuyQuote[j].Price, ",", "", -1) - price, err = strconv.ParseFloat(p, 64) - if err != nil { - b.Websocket.DataHandler <- err - continue - } - a := strings.Replace(t.Data.BuyQuote[j].Size, ",", "", -1) - amount, err = strconv.ParseFloat(a, 64) - if err != nil { - b.Websocket.DataHandler <- err - continue - } - newOB.Bids = append(newOB.Bids, orderbook.Item{ - Price: price, - Amount: amount, - }) - } - newOB.AssetType = asset.Spot - newOB.Pair = currency.NewPairFromString(t.Topic[strings.Index(t.Topic, ":")+1 : strings.Index(t.Topic, "_")]) - newOB.ExchangeName = b.Name - err = b.Websocket.Orderbook.LoadSnapshot(&newOB) - if err != nil { - b.Websocket.DataHandler <- err - continue - } - b.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{Pair: newOB.Pair, - Asset: asset.Spot, - Exchange: b.Name} - default: - log.Warnf(log.ExchangeSys, - "%s: unhandled websocket response: %s", b.Name, resp.Raw) } } } } +func (b *BTSE) wsHandleData(respRaw []byte) error { + type Result map[string]interface{} + var result Result + err := json.Unmarshal(respRaw, &result) + if err != nil { + return err + } + switch { + case result["topic"] == "notificationApi": + var notification wsNotification + err = json.Unmarshal(respRaw, ¬ification) + if err != nil { + return err + } + for i := range notification.Data { + var oType order.Type + var oSide order.Side + var oStatus order.Status + oType, err = order.StringToOrderType(notification.Data[i].Type) + if err != nil { + b.Websocket.DataHandler <- order.ClassificationError{ + Exchange: b.Name, + OrderID: notification.Data[i].OrderID, + Err: err, + } + } + oSide, err = order.StringToOrderSide(notification.Data[i].OrderMode) + if err != nil { + b.Websocket.DataHandler <- order.ClassificationError{ + Exchange: b.Name, + OrderID: notification.Data[i].OrderID, + Err: err, + } + } + oStatus, err = stringToOrderStatus(notification.Data[i].Status) + if err != nil { + b.Websocket.DataHandler <- order.ClassificationError{ + Exchange: b.Name, + OrderID: notification.Data[i].OrderID, + Err: err, + } + } + p := currency.NewPairFromString(notification.Data[i].Symbol) + var a asset.Item + a, err = b.GetPairAssetType(p) + if err != nil { + return err + } + b.Websocket.DataHandler <- &order.Detail{ + Price: notification.Data[i].Price, + Amount: notification.Data[i].Size, + TriggerPrice: notification.Data[i].TriggerPrice, + Exchange: b.Name, + ID: notification.Data[i].OrderID, + Type: oType, + Side: oSide, + Status: oStatus, + AssetType: a, + Date: time.Unix(0, notification.Data[i].Timestamp*int64(time.Millisecond)), + Pair: p, + } + } + + case strings.Contains(result["topic"].(string), "tradeHistory"): + var tradeHistory wsTradeHistory + err = json.Unmarshal(respRaw, &tradeHistory) + if err != nil { + return err + } + for x := range tradeHistory.Data { + side := order.Buy + if tradeHistory.Data[x].Gain == -1 { + side = order.Sell + } + p := currency.NewPairFromString(strings.Replace(tradeHistory.Topic, "tradeHistory:", "", 1)) + var a asset.Item + a, err = b.GetPairAssetType(p) + if err != nil { + return err + } + b.Websocket.DataHandler <- wshandler.TradeData{ + Timestamp: time.Unix(0, tradeHistory.Data[x].TransactionTime*int64(time.Millisecond)), + CurrencyPair: p, + AssetType: a, + Exchange: b.Name, + Price: tradeHistory.Data[x].Price, + Amount: tradeHistory.Data[x].Amount, + Side: side, + } + } + case strings.Contains(result["topic"].(string), "orderBookApi"): + var t wsOrderBook + err = json.Unmarshal(respRaw, &t) + if err != nil { + return err + } + var newOB orderbook.Base + var price, amount float64 + for i := range t.Data.SellQuote { + p := strings.Replace(t.Data.SellQuote[i].Price, ",", "", -1) + price, err = strconv.ParseFloat(p, 64) + if err != nil { + return err + } + a := strings.Replace(t.Data.SellQuote[i].Size, ",", "", -1) + amount, err = strconv.ParseFloat(a, 64) + if err != nil { + return err + } + newOB.Asks = append(newOB.Asks, orderbook.Item{ + Price: price, + Amount: amount, + }) + } + for j := range t.Data.BuyQuote { + p := strings.Replace(t.Data.BuyQuote[j].Price, ",", "", -1) + price, err = strconv.ParseFloat(p, 64) + if err != nil { + return err + } + a := strings.Replace(t.Data.BuyQuote[j].Size, ",", "", -1) + amount, err = strconv.ParseFloat(a, 64) + if err != nil { + return err + } + newOB.Bids = append(newOB.Bids, orderbook.Item{ + Price: price, + Amount: amount, + }) + } + p := currency.NewPairFromString(t.Topic[strings.Index(t.Topic, ":")+1 : strings.Index(t.Topic, "_")]) + var a asset.Item + a, err = b.GetPairAssetType(p) + if err != nil { + return err + } + newOB.Pair = p + newOB.AssetType = a + newOB.ExchangeName = b.Name + err = b.Websocket.Orderbook.LoadSnapshot(&newOB) + if err != nil { + return err + } + b.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{Pair: newOB.Pair, + Asset: a, + Exchange: b.Name} + default: + b.Websocket.DataHandler <- wshandler.UnhandledMessageWarning{Message: b.Name + wshandler.UnhandledMessage + string(respRaw)} + return nil + } + return nil +} + // GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions() func (b *BTSE) GenerateDefaultSubscriptions() { var channels = []string{"orderBookApi:%s_0", "tradeHistory:%s"} pairs := b.GetEnabledPairs(asset.Spot) var subscriptions []wshandler.WebsocketChannelSubscription + if b.Websocket.CanUseAuthenticatedEndpoints() { + subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{ + Channel: "notificationApi", + }) + } for i := range channels { for j := range pairs { subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{ @@ -179,6 +297,7 @@ func (b *BTSE) Subscribe(channelToSubscribe wshandler.WebsocketChannelSubscripti var sub wsSub sub.Operation = "subscribe" sub.Arguments = []string{channelToSubscribe.Channel} + return b.WebsocketConn.SendJSONMessage(sub) } diff --git a/exchanges/btse/btse_wrapper.go b/exchanges/btse/btse_wrapper.go index ca97da79..4cfdffc8 100644 --- a/exchanges/btse/btse_wrapper.go +++ b/exchanges/btse/btse_wrapper.go @@ -95,6 +95,8 @@ func (b *BTSE) SetDefaults() { TradeFetching: true, Subscribe: true, Unsubscribe: true, + GetOrders: true, + GetOrder: true, }, WithdrawPermissions: exchange.NoAPIWithdrawalMethods, }, @@ -359,8 +361,8 @@ func (b *BTSE) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) { r, err := b.CreateOrder(s.Amount, s.Price, - s.OrderSide.String(), - s.OrderType.String(), + s.Side.String(), + s.Type.String(), b.FormatExchangeCurrency(s.Pair, asset.Spot).String(), goodTillCancel, s.ClientID) @@ -372,7 +374,7 @@ func (b *BTSE) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) { resp.IsOrderPlaced = true resp.OrderID = *r } - if s.OrderType == order.Market { + if s.Type == order.Market { resp.FullyMatched = true } return resp, nil @@ -386,8 +388,8 @@ func (b *BTSE) ModifyOrder(action *order.Modify) (string, error) { // CancelOrder cancels an order by its corresponding ID number func (b *BTSE) CancelOrder(order *order.Cancel) error { - r, err := b.CancelExistingOrder(order.OrderID, - b.FormatExchangeCurrency(order.CurrencyPair, + r, err := b.CancelExistingOrder(order.ID, + b.FormatExchangeCurrency(order.Pair, asset.Spot).String()) if err != nil { return err @@ -415,7 +417,7 @@ func (b *BTSE) CancelAllOrders(orderCancellation *order.Cancel) (order.CancelAll resp.Status = make(map[string]string) for x := range markets { - strPair := b.FormatExchangeCurrency(orderCancellation.CurrencyPair, + strPair := b.FormatExchangeCurrency(orderCancellation.Pair, orderCancellation.AssetType).String() checkPair := currency.NewPairWithDelimiter(markets[x].BaseCurrency, markets[x].QuoteCurrency, @@ -462,18 +464,18 @@ func (b *BTSE) GetOrderInfo(orderID string) (order.Detail, error) { side = order.Sell } - od.CurrencyPair = currency.NewPairDelimiter(o[i].Symbol, + od.Pair = currency.NewPairDelimiter(o[i].Symbol, b.GetPairFormat(asset.Spot, false).Delimiter) od.Exchange = b.Name od.Amount = o[i].Amount od.ID = o[i].ID - od.OrderDate, err = parseOrderTime(o[i].CreatedAt) + od.Date, err = parseOrderTime(o[i].CreatedAt) if err != nil { log.Errorf(log.ExchangeSys, "%s GetOrderInfo unable to parse time: %s\n", b.Name, err) } - od.OrderSide = side - od.OrderType = order.Type(strings.ToUpper(o[i].Type)) + od.Side = side + od.Type = order.Type(strings.ToUpper(o[i].Type)) od.Price = o[i].Price od.Status = order.Status(o[i].Status) @@ -555,16 +557,16 @@ func (b *BTSE) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, err } openOrder := order.Detail{ - CurrencyPair: currency.NewPairDelimiter(resp[i].Symbol, + Pair: currency.NewPairDelimiter(resp[i].Symbol, b.GetPairFormat(asset.Spot, false).Delimiter), - Exchange: b.Name, - Amount: resp[i].Amount, - ID: resp[i].ID, - OrderDate: tm, - OrderSide: side, - OrderType: order.Type(strings.ToUpper(resp[i].Type)), - Price: resp[i].Price, - Status: order.Status(resp[i].Status), + Exchange: b.Name, + Amount: resp[i].Amount, + ID: resp[i].ID, + Date: tm, + Side: side, + Type: order.Type(strings.ToUpper(resp[i].Type)), + Price: resp[i].Price, + Status: order.Status(resp[i].Status), } fills, err := b.GetFills(resp[i].ID, "", "", "", "", "") @@ -597,9 +599,9 @@ func (b *BTSE) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, err orders = append(orders, openOrder) } - order.FilterOrdersByType(&orders, req.OrderType) + order.FilterOrdersByType(&orders, req.Type) order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) - order.FilterOrdersBySide(&orders, req.OrderSide) + order.FilterOrdersBySide(&orders, req.Side) return orders, nil } diff --git a/exchanges/coinbasepro/coinbasepro_test.go b/exchanges/coinbasepro/coinbasepro_test.go index 8b3ef88d..39ffebb1 100644 --- a/exchanges/coinbasepro/coinbasepro_test.go +++ b/exchanges/coinbasepro/coinbasepro_test.go @@ -51,7 +51,8 @@ func TestMain(m *testing.M) { if err != nil { log.Fatal("CoinbasePro setup error", err) } - + c.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride() + c.Websocket.TrafficAlert = sharedtestvalues.GetWebsocketStructChannelOverride() os.Exit(m.Run()) } @@ -426,8 +427,8 @@ func TestFormatWithdrawPermissions(t *testing.T) { func TestGetActiveOrders(t *testing.T) { var getOrdersRequest = order.GetOrdersRequest{ - OrderType: order.AnyType, - Currencies: []currency.Pair{currency.NewPair(currency.BTC, + Type: order.AnyType, + Pairs: []currency.Pair{currency.NewPair(currency.BTC, currency.LTC)}, } @@ -441,8 +442,8 @@ func TestGetActiveOrders(t *testing.T) { func TestGetOrderHistory(t *testing.T) { var getOrdersRequest = order.GetOrdersRequest{ - OrderType: order.AnyType, - Currencies: []currency.Pair{currency.NewPair(currency.BTC, + Type: order.AnyType, + Pairs: []currency.Pair{currency.NewPair(currency.BTC, currency.LTC)}, } @@ -471,11 +472,11 @@ func TestSubmitOrder(t *testing.T) { Base: currency.BTC, Quote: currency.USD, }, - OrderSide: order.Buy, - OrderType: order.Limit, - Price: 1, - Amount: 1, - ClientID: "meowOrder", + Side: order.Buy, + Type: order.Limit, + Price: 1, + Amount: 1, + ClientID: "meowOrder", } response, err := c.SubmitOrder(orderSubmission) if areTestAPIKeysSet() && (err != nil || !response.IsOrderPlaced) { @@ -492,10 +493,10 @@ func TestCancelExchangeOrder(t *testing.T) { currencyPair := currency.NewPair(currency.LTC, currency.BTC) var orderCancellation = &order.Cancel{ - OrderID: "1", + ID: "1", WalletAddress: core.BitcoinDonationAddress, AccountID: "1", - CurrencyPair: currencyPair, + Pair: currencyPair, } err := c.CancelOrder(orderCancellation) @@ -514,10 +515,10 @@ func TestCancelAllExchangeOrders(t *testing.T) { currencyPair := currency.NewPair(currency.LTC, currency.BTC) var orderCancellation = &order.Cancel{ - OrderID: "1", + ID: "1", WalletAddress: core.BitcoinDonationAddress, AccountID: "1", - CurrencyPair: currencyPair, + Pair: currencyPair, } resp, err := c.CancelAllOrders(orderCancellation) @@ -641,7 +642,7 @@ func TestWsAuth(t *testing.T) { } c.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride() c.Websocket.TrafficAlert = sharedtestvalues.GetWebsocketStructChannelOverride() - go c.WsHandleData() + go c.wsReadData() err = c.Subscribe(wshandler.WebsocketChannelSubscription{ Channel: "user", Currency: currency.NewPairFromString(testPair), @@ -657,3 +658,311 @@ func TestWsAuth(t *testing.T) { } timer.Stop() } + +func TestWsSubscribe(t *testing.T) { + pressXToJSON := []byte(`{ + "type": "subscriptions", + "channels": [ + { + "name": "level2", + "product_ids": [ + "ETH-USD", + "ETH-EUR" + ] + }, + { + "name": "heartbeat", + "product_ids": [ + "ETH-USD", + "ETH-EUR" + ] + }, + { + "name": "ticker", + "product_ids": [ + "ETH-USD", + "ETH-EUR", + "ETH-BTC" + ] + } + ] + }`) + err := c.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsHeartbeat(t *testing.T) { + pressXToJSON := []byte(`{ + "type": "heartbeat", + "sequence": 90, + "last_trade_id": 20, + "product_id": "BTC-USD", + "time": "2014-11-07T08:19:28.464459Z" + }`) + err := c.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsStatus(t *testing.T) { + pressXToJSON := []byte(`{ + "type": "status", + "products": [ + { + "id": "BTC-USD", + "base_currency": "BTC", + "quote_currency": "USD", + "base_min_size": "0.001", + "base_max_size": "70", + "base_increment": "0.00000001", + "quote_increment": "0.01", + "display_name": "BTC/USD", + "status": "online", + "status_message": null, + "min_market_funds": "10", + "max_market_funds": "1000000", + "post_only": false, + "limit_only": false, + "cancel_only": false + } + ], + "currencies": [ + { + "id": "USD", + "name": "United States Dollar", + "min_size": "0.01000000", + "status": "online", + "status_message": null, + "max_precision": "0.01", + "convertible_to": ["USDC"], "details": {} + }, + { + "id": "USDC", + "name": "USD Coin", + "min_size": "0.00000100", + "status": "online", + "status_message": null, + "max_precision": "0.000001", + "convertible_to": ["USD"], "details": {} + }, + { + "id": "BTC", + "name": "Bitcoin", + "min_size": "0.00000001", + "status": "online", + "status_message": null, + "max_precision": "0.00000001", + "convertible_to": [] + } + ] +}`) + err := c.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsTicker(t *testing.T) { + pressXToJSON := []byte(`{ + "type": "ticker", + "trade_id": 20153558, + "sequence": 3262786978, + "time": "2017-09-02T17:05:49.250000Z", + "product_id": "BTC-USD", + "price": "4388.01000000", + "side": "buy", + "last_size": "0.03000000", + "best_bid": "4388", + "best_ask": "4388.01" +}`) + err := c.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsOrderbook(t *testing.T) { + pressXToJSON := []byte(`{ + "type": "snapshot", + "product_id": "BTC-USD", + "bids": [["10101.10", "0.45054140"]], + "asks": [["10102.55", "0.57753524"]] +}`) + err := c.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } + + pressXToJSON = []byte(`{ + "type": "l2update", + "product_id": "BTC-USD", + "time": "2019-08-14T20:42:27.265Z", + "changes": [ + [ + "buy", + "10101.80000000", + "0.162567" + ] + ] +}`) + err = c.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsOrders(t *testing.T) { + pressXToJSON := []byte(`{ + "type": "received", + "time": "2014-11-07T08:19:27.028459Z", + "product_id": "BTC-USD", + "sequence": 10, + "order_id": "d50ec984-77a8-460a-b958-66f114b0de9b", + "size": "1.34", + "price": "502.1", + "side": "buy", + "order_type": "limit" +}`) + err := c.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } + + pressXToJSON = []byte(`{ + "type": "received", + "time": "2014-11-09T08:19:27.028459Z", + "product_id": "BTC-USD", + "sequence": 12, + "order_id": "dddec984-77a8-460a-b958-66f114b0de9b", + "funds": "3000.234", + "side": "buy", + "order_type": "market" +}`) + err = c.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } + + pressXToJSON = []byte(`{ + "type": "open", + "time": "2014-11-07T08:19:27.028459Z", + "product_id": "BTC-USD", + "sequence": 10, + "order_id": "d50ec984-77a8-460a-b958-66f114b0de9b", + "price": "200.2", + "remaining_size": "1.00", + "side": "sell" +}`) + err = c.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } + + pressXToJSON = []byte(`{ + "type": "done", + "time": "2014-11-07T08:19:27.028459Z", + "product_id": "BTC-USD", + "sequence": 10, + "price": "200.2", + "order_id": "d50ec984-77a8-460a-b958-66f114b0de9b", + "reason": "filled", + "side": "sell", + "remaining_size": "0" +}`) + err = c.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } + + pressXToJSON = []byte(`{ + "type": "match", + "trade_id": 10, + "sequence": 50, + "maker_order_id": "ac928c66-ca53-498f-9c13-a110027a60e8", + "taker_order_id": "132fb6ae-456b-4654-b4e0-d681ac05cea1", + "time": "2014-11-07T08:19:27.028459Z", + "product_id": "BTC-USD", + "size": "5.23512", + "price": "400.23", + "side": "sell" +}`) + err = c.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } + + pressXToJSON = []byte(`{ + "type": "change", + "time": "2014-11-07T08:19:27.028459Z", + "sequence": 80, + "order_id": "ac928c66-ca53-498f-9c13-a110027a60e8", + "product_id": "BTC-USD", + "new_size": "5.23512", + "old_size": "12.234412", + "price": "400.23", + "side": "sell" +}`) + err = c.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } + pressXToJSON = []byte(`{ + "type": "change", + "time": "2014-11-07T08:19:27.028459Z", + "sequence": 80, + "order_id": "ac928c66-ca53-498f-9c13-a110027a60e8", + "product_id": "BTC-USD", + "new_funds": "5.23512", + "old_funds": "12.234412", + "price": "400.23", + "side": "sell" +}`) + err = c.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } + pressXToJSON = []byte(`{ + "type": "activate", + "product_id": "BTC-USD", + "timestamp": "1483736448.299000", + "user_id": "12", + "profile_id": "30000727-d308-cf50-7b1c-c06deb1934fc", + "order_id": "7b52009b-64fd-0a2a-49e6-d8a939753077", + "stop_type": "entry", + "side": "buy", + "stop_price": "80", + "size": "2", + "funds": "50", + "taker_fee_rate": "0.0025", + "private": true +}`) + err = c.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestStatusToStandardStatus(t *testing.T) { + type TestCases struct { + Case string + Result order.Status + } + testCases := []TestCases{ + {Case: "received", Result: order.New}, + {Case: "open", Result: order.Active}, + {Case: "done", Result: order.Filled}, + {Case: "match", Result: order.PartiallyFilled}, + {Case: "change", Result: order.Active}, + {Case: "activate", Result: order.Active}, + {Case: "LOL", Result: order.UnknownStatus}, + } + for i := range testCases { + result, _ := statusToStandardStatus(testCases[i].Case) + if result != testCases[i].Result { + t.Errorf("Exepcted: %v, received: %v", testCases[i].Result, result) + } + } +} diff --git a/exchanges/coinbasepro/coinbasepro_types.go b/exchanges/coinbasepro/coinbasepro_types.go index 05ea954b..f56080f4 100644 --- a/exchanges/coinbasepro/coinbasepro_types.go +++ b/exchanges/coinbasepro/coinbasepro_types.go @@ -365,70 +365,34 @@ type WsChannels struct { ProductIDs []string `json:"product_ids"` } -// WebsocketReceived holds websocket received values -type WebsocketReceived struct { - Type string `json:"type"` - OrderID string `json:"order_id"` - OrderType string `json:"order_type"` - Size float64 `json:"size,string"` - Price float64 `json:"price,omitempty,string"` - Funds float64 `json:"funds,omitempty,string"` - Side string `json:"side"` - ClientOID string `json:"client_oid"` - ProductID string `json:"product_id"` - Sequence int64 `json:"sequence"` - Time string `json:"time"` -} - -// WebsocketOpen collates open orders -type WebsocketOpen struct { - Type string `json:"type"` - Side string `json:"side"` - Price float64 `json:"price,string"` - OrderID string `json:"order_id"` - RemainingSize float64 `json:"remaining_size,string"` - ProductID string `json:"product_id"` - Sequence int64 `json:"sequence"` - Time string `json:"time"` -} - -// WebsocketDone holds finished order information -type WebsocketDone struct { - Type string `json:"type"` - Side string `json:"side"` - OrderID string `json:"order_id"` - Reason string `json:"reason"` - ProductID string `json:"product_id"` - Price float64 `json:"price,string"` - RemainingSize float64 `json:"remaining_size,string"` - Sequence int64 `json:"sequence"` - Time string `json:"time"` -} - -// WebsocketMatch holds match information -type WebsocketMatch struct { - Type string `json:"type"` - TradeID int `json:"trade_id"` - MakerOrderID string `json:"maker_order_id"` - TakerOrderID string `json:"taker_order_id"` - Side string `json:"side"` - Size float64 `json:"size,string"` - Price float64 `json:"price,string"` - ProductID string `json:"product_id"` - Sequence int64 `json:"sequence"` - Time string `json:"time"` -} - -// WebsocketChange holds change information -type WebsocketChange struct { - Type string `json:"type"` - Time string `json:"time"` - Sequence int `json:"sequence"` - OrderID string `json:"order_id"` - NewSize float64 `json:"new_size,string"` - OldSize float64 `json:"old_size,string"` - Price float64 `json:"price,string"` - Side string `json:"side"` +// wsOrderReceived holds websocket received values +type wsOrderReceived struct { + Type string `json:"type"` + OrderID string `json:"order_id"` + OrderType string `json:"order_type"` + Size float64 `json:"size,string"` + Price float64 `json:"price,omitempty,string"` + Funds float64 `json:"funds,omitempty,string"` + Side string `json:"side"` + ClientOID string `json:"client_oid"` + ProductID string `json:"product_id"` + Sequence int64 `json:"sequence"` + Time time.Time `json:"time"` + RemainingSize float64 `json:"remaining_size,string"` + NewSize float64 `json:"new_size,string"` + OldSize float64 `json:"old_size,string"` + Reason string `json:"reason"` + Timestamp float64 `json:"timestamp,string"` + UserID string `json:"user_id"` + ProfileID string `json:"profile_id"` + StopType string `json:"stop_type"` + StopPrice float64 `json:"stop_price,string"` + TakerFeeRate float64 `json:"taker_fee_rate,string"` + Private bool `json:"private"` + TradeID int64 `json:"trade_id"` + MakerOrderID string `json:"maker_order_id"` + TakerOrderID string `json:"taker_order_id"` + TakerUserID string `json:"taker_user_id"` } // WebsocketHeartBeat defines JSON response for a heart beat message @@ -475,19 +439,39 @@ type WebsocketL2Update struct { Changes [][]interface{} `json:"changes"` } -// WebsocketActivate an activate message is sent when a stop order is placed -type WebsocketActivate struct { - Type string `json:"type"` - ProductID string `json:"product_id"` - Timestamp string `json:"timestamp"` - UserID string `json:"user_id"` - ProfileID string `json:"profile_id"` - OrderID string `json:"order_id"` - StopType string `json:"stop_type"` - Side string `json:"side"` - StopPrice float64 `json:"stop_price,string"` - Size float64 `json:"size,string"` - Funds float64 `json:"funds,string"` - TakerFeeRate float64 `json:"taker_fee_rate,string"` - Private bool `json:"private"` +type wsMsgType struct { + Type string `json:"type"` + Sequence int64 `json:"sequence"` + ProductID string `json:"product_id"` +} + +type wsStatus struct { + Currencies []struct { + ConvertibleTo []string `json:"convertible_to"` + Details struct{} `json:"details"` + ID string `json:"id"` + MaxPrecision float64 `json:"max_precision,string"` + MinSize float64 `json:"min_size,string"` + Name string `json:"name"` + Status string `json:"status"` + StatusMessage interface{} `json:"status_message"` + } `json:"currencies"` + Products []struct { + BaseCurrency string `json:"base_currency"` + BaseIncrement float64 `json:"base_increment,string"` + BaseMaxSize float64 `json:"base_max_size,string"` + BaseMinSize float64 `json:"base_min_size,string"` + CancelOnly bool `json:"cancel_only"` + DisplayName string `json:"display_name"` + ID string `json:"id"` + LimitOnly bool `json:"limit_only"` + MaxMarketFunds float64 `json:"max_market_funds,string"` + MinMarketFunds float64 `json:"min_market_funds,string"` + PostOnly bool `json:"post_only"` + QuoteCurrency string `json:"quote_currency"` + QuoteIncrement float64 `json:"quote_increment,string"` + Status string `json:"status"` + StatusMessage interface{} `json:"status_message"` + } `json:"products"` + Type string `json:"type"` } diff --git a/exchanges/coinbasepro/coinbasepro_websocket.go b/exchanges/coinbasepro/coinbasepro_websocket.go index e57a1c47..a6b333ff 100644 --- a/exchanges/coinbasepro/coinbasepro_websocket.go +++ b/exchanges/coinbasepro/coinbasepro_websocket.go @@ -3,11 +3,13 @@ package coinbasepro import ( "encoding/json" "errors" + "fmt" "net/http" "strconv" "time" "github.com/gorilla/websocket" + "github.com/thrasher-corp/gocryptotrader/common/convert" "github.com/thrasher-corp/gocryptotrader/common/crypto" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" @@ -35,13 +37,13 @@ func (c *CoinbasePro) WsConnect() error { } c.GenerateDefaultSubscriptions() - go c.WsHandleData() + go c.wsReadData() return nil } -// WsHandleData handles read data from websocket connection -func (c *CoinbasePro) WsHandleData() { +// wsReadData receives and passes on websocket messages for processing +func (c *CoinbasePro) wsReadData() { c.Websocket.Wg.Add(1) defer func() { @@ -59,127 +61,206 @@ func (c *CoinbasePro) WsHandleData() { return } c.Websocket.TrafficAlert <- struct{}{} - - type MsgType struct { - Type string `json:"type"` - Sequence int64 `json:"sequence"` - ProductID string `json:"product_id"` - } - - msgType := MsgType{} - err = json.Unmarshal(resp.Raw, &msgType) + err = c.wsHandleData(resp.Raw) if err != nil { c.Websocket.DataHandler <- err - continue - } - - if msgType.Type == "subscriptions" || msgType.Type == "heartbeat" { - continue - } - - switch msgType.Type { - case "error": - c.Websocket.DataHandler <- errors.New(string(resp.Raw)) - - case "ticker": - wsTicker := WebsocketTicker{} - err := json.Unmarshal(resp.Raw, &wsTicker) - if err != nil { - c.Websocket.DataHandler <- err - continue - } - - c.Websocket.DataHandler <- &ticker.Price{ - LastUpdated: wsTicker.Time, - Pair: wsTicker.ProductID, - AssetType: asset.Spot, - ExchangeName: c.Name, - Open: wsTicker.Open24H, - High: wsTicker.High24H, - Low: wsTicker.Low24H, - Last: wsTicker.Price, - Volume: wsTicker.Volume24H, - Bid: wsTicker.BestBid, - Ask: wsTicker.BestAsk, - } - - case "snapshot": - snapshot := WebsocketOrderbookSnapshot{} - err := json.Unmarshal(resp.Raw, &snapshot) - if err != nil { - c.Websocket.DataHandler <- err - continue - } - - err = c.ProcessSnapshot(&snapshot) - if err != nil { - c.Websocket.DataHandler <- err - continue - } - - case "l2update": - update := WebsocketL2Update{} - err := json.Unmarshal(resp.Raw, &update) - if err != nil { - c.Websocket.DataHandler <- err - continue - } - - err = c.ProcessUpdate(update) - if err != nil { - c.Websocket.DataHandler <- err - continue - } - case "received": - // We currently use l2update to calculate orderbook changes - received := WebsocketReceived{} - err := json.Unmarshal(resp.Raw, &received) - if err != nil { - c.Websocket.DataHandler <- err - continue - } - c.Websocket.DataHandler <- received - case "open": - // We currently use l2update to calculate orderbook changes - open := WebsocketOpen{} - err := json.Unmarshal(resp.Raw, &open) - if err != nil { - c.Websocket.DataHandler <- err - continue - } - c.Websocket.DataHandler <- open - case "done": - // We currently use l2update to calculate orderbook changes - done := WebsocketDone{} - err := json.Unmarshal(resp.Raw, &done) - if err != nil { - c.Websocket.DataHandler <- err - continue - } - c.Websocket.DataHandler <- done - case "change": - // We currently use l2update to calculate orderbook changes - change := WebsocketChange{} - err := json.Unmarshal(resp.Raw, &change) - if err != nil { - c.Websocket.DataHandler <- err - continue - } - c.Websocket.DataHandler <- change - case "activate": - // We currently use l2update to calculate orderbook changes - activate := WebsocketActivate{} - err := json.Unmarshal(resp.Raw, &activate) - if err != nil { - c.Websocket.DataHandler <- err - continue - } - c.Websocket.DataHandler <- activate } } } } +func (c *CoinbasePro) wsHandleData(respRaw []byte) error { + msgType := wsMsgType{} + err := json.Unmarshal(respRaw, &msgType) + if err != nil { + return err + } + + if msgType.Type == "subscriptions" || msgType.Type == "heartbeat" { + return nil + } + + switch msgType.Type { + case "status": + var status wsStatus + err = json.Unmarshal(respRaw, &status) + if err != nil { + return err + } + c.Websocket.DataHandler <- status + case "error": + c.Websocket.DataHandler <- errors.New(string(respRaw)) + case "ticker": + wsTicker := WebsocketTicker{} + err := json.Unmarshal(respRaw, &wsTicker) + if err != nil { + return err + } + + c.Websocket.DataHandler <- &ticker.Price{ + LastUpdated: wsTicker.Time, + Pair: wsTicker.ProductID, + AssetType: asset.Spot, + ExchangeName: c.Name, + Open: wsTicker.Open24H, + High: wsTicker.High24H, + Low: wsTicker.Low24H, + Last: wsTicker.Price, + Volume: wsTicker.Volume24H, + Bid: wsTicker.BestBid, + Ask: wsTicker.BestAsk, + } + + case "snapshot": + snapshot := WebsocketOrderbookSnapshot{} + err := json.Unmarshal(respRaw, &snapshot) + if err != nil { + return err + } + + err = c.ProcessSnapshot(&snapshot) + if err != nil { + return err + } + + case "l2update": + update := WebsocketL2Update{} + err := json.Unmarshal(respRaw, &update) + if err != nil { + return err + } + + err = c.ProcessUpdate(update) + if err != nil { + return err + } + // the following cases contains data to synchronise authenticated orders + // subscribing to the "full" channel will consider ALL cbp orders as + // personal orders + // remove sending &order.Detail to the datahandler if you wish to subscribe to the + // "full" channel + case "received", "open", "done", "change", "activate": + var wsOrder wsOrderReceived + err := json.Unmarshal(respRaw, &wsOrder) + if err != nil { + return err + } + var oType order.Type + var oSide order.Side + var oStatus order.Status + oType, err = order.StringToOrderType(wsOrder.OrderType) + if err != nil { + c.Websocket.DataHandler <- order.ClassificationError{ + Exchange: c.Name, + OrderID: wsOrder.OrderID, + Err: err, + } + } + oSide, err = order.StringToOrderSide(wsOrder.Side) + if err != nil { + c.Websocket.DataHandler <- order.ClassificationError{ + Exchange: c.Name, + OrderID: wsOrder.OrderID, + Err: err, + } + } + oStatus, err = statusToStandardStatus(wsOrder.Type) + if err != nil { + c.Websocket.DataHandler <- order.ClassificationError{ + Exchange: c.Name, + OrderID: wsOrder.OrderID, + Err: err, + } + } + if wsOrder.Reason == "canceled" { + oStatus = order.Cancelled + } + ts := wsOrder.Time + if wsOrder.Type == "activate" { + var one, two int64 + one, two, err = convert.SplitFloatDecimals(wsOrder.Timestamp) + if err != nil { + return err + } + ts = time.Unix(one, two) + } + + var p currency.Pair + var a asset.Item + p, a, err = c.GetRequestFormattedPairAndAssetType(wsOrder.ProductID) + if err != nil { + return err + } + c.Websocket.DataHandler <- &order.Detail{ + HiddenOrder: wsOrder.Private, + Price: wsOrder.Price, + Amount: wsOrder.Size, + TriggerPrice: wsOrder.StopPrice, + ExecutedAmount: wsOrder.Size - wsOrder.RemainingSize, + RemainingAmount: wsOrder.RemainingSize, + Fee: wsOrder.TakerFeeRate, + Exchange: c.Name, + ID: wsOrder.OrderID, + AccountID: wsOrder.ProfileID, + ClientID: c.API.Credentials.ClientID, + Type: oType, + Side: oSide, + Status: oStatus, + AssetType: a, + Date: ts, + Pair: p, + } + case "match": + var wsOrder wsOrderReceived + err := json.Unmarshal(respRaw, &wsOrder) + if err != nil { + return err + } + oSide, err := order.StringToOrderSide(wsOrder.Side) + if err != nil { + c.Websocket.DataHandler <- order.ClassificationError{ + Exchange: c.Name, + Err: err, + } + } + c.Websocket.DataHandler <- &order.Detail{ + ID: wsOrder.OrderID, + Trades: []order.TradeHistory{ + { + Price: wsOrder.Price, + Amount: wsOrder.Size, + Exchange: c.Name, + TID: strconv.FormatInt(wsOrder.TradeID, 10), + Side: oSide, + Timestamp: wsOrder.Time, + IsMaker: wsOrder.TakerUserID == "", + }, + }, + } + default: + c.Websocket.DataHandler <- wshandler.UnhandledMessageWarning{Message: c.Name + wshandler.UnhandledMessage + string(respRaw)} + return nil + } + return nil +} + +func statusToStandardStatus(stat string) (order.Status, error) { + switch stat { + case "received": + return order.New, nil + case "open": + return order.Active, nil + case "done": + return order.Filled, nil + case "match": + return order.PartiallyFilled, nil + case "change", "activate": + return order.Active, nil + default: + return order.UnknownStatus, fmt.Errorf("%s not recognised as status type", stat) + } +} + // ProcessSnapshot processes the initial orderbook snap shot func (c *CoinbasePro) ProcessSnapshot(snapshot *WebsocketOrderbookSnapshot) error { var base orderbook.Base diff --git a/exchanges/coinbasepro/coinbasepro_wrapper.go b/exchanges/coinbasepro/coinbasepro_wrapper.go index b9d1616d..cfc3e9c8 100644 --- a/exchanges/coinbasepro/coinbasepro_wrapper.go +++ b/exchanges/coinbasepro/coinbasepro_wrapper.go @@ -106,6 +106,8 @@ func (c *CoinbasePro) SetDefaults() { Unsubscribe: true, AuthenticatedEndpoints: true, MessageSequenceNumbers: true, + GetOrders: true, + GetOrder: true, }, WithdrawPermissions: exchange.AutoWithdrawCryptoWithAPIPermission | exchange.AutoWithdrawFiatWithAPIPermission, @@ -396,19 +398,19 @@ func (c *CoinbasePro) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) var response string var err error - switch s.OrderType { + switch s.Type { case order.Market: response, err = c.PlaceMarketOrder("", s.Amount, s.Amount, - s.OrderSide.Lower(), + s.Side.Lower(), c.FormatExchangeCurrency(s.Pair, asset.Spot).String(), "") case order.Limit: response, err = c.PlaceLimitOrder("", s.Price, s.Amount, - s.OrderSide.Lower(), + s.Side.Lower(), "", "", c.FormatExchangeCurrency(s.Pair, asset.Spot).String(), @@ -420,7 +422,7 @@ func (c *CoinbasePro) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) if err != nil { return submitOrderResponse, err } - if s.OrderType == order.Market { + if s.Type == order.Market { submitOrderResponse.FullyMatched = true } if response != "" { @@ -440,7 +442,7 @@ func (c *CoinbasePro) ModifyOrder(action *order.Modify) (string, error) { // CancelOrder cancels an order by its corresponding ID number func (c *CoinbasePro) CancelOrder(order *order.Cancel) error { - return c.CancelExistingOrder(order.OrderID) + return c.CancelExistingOrder(order.ID) } // CancelAllOrders cancels all orders associated with a currency pair @@ -532,9 +534,9 @@ func (c *CoinbasePro) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, er // GetActiveOrders retrieves any orders that are active/open func (c *CoinbasePro) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, error) { var respOrders []GeneralizedOrderResponse - for i := range req.Currencies { + for i := range req.Pairs { resp, err := c.GetOrders([]string{"open", "pending", "active"}, - c.FormatExchangeCurrency(req.Currencies[i], asset.Spot).String()) + c.FormatExchangeCurrency(req.Pairs[i], asset.Spot).String()) if err != nil { return nil, err } @@ -561,17 +563,17 @@ func (c *CoinbasePro) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Deta ID: respOrders[i].ID, Amount: respOrders[i].Size, ExecutedAmount: respOrders[i].FilledSize, - OrderType: orderType, - OrderDate: orderDate, - OrderSide: orderSide, - CurrencyPair: curr, + Type: orderType, + Date: orderDate, + Side: orderSide, + Pair: curr, Exchange: c.Name, }) } - order.FilterOrdersByType(&orders, req.OrderType) + order.FilterOrdersByType(&orders, req.Type) order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) - order.FilterOrdersBySide(&orders, req.OrderSide) + order.FilterOrdersBySide(&orders, req.Side) return orders, nil } @@ -579,9 +581,9 @@ func (c *CoinbasePro) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Deta // Can Limit response to specific order status func (c *CoinbasePro) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail, error) { var respOrders []GeneralizedOrderResponse - for i := range req.Currencies { + for i := range req.Pairs { resp, err := c.GetOrders([]string{"done", "settled"}, - c.FormatExchangeCurrency(req.Currencies[i], asset.Spot).String()) + c.FormatExchangeCurrency(req.Pairs[i], asset.Spot).String()) if err != nil { return nil, err } @@ -608,17 +610,17 @@ func (c *CoinbasePro) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Deta ID: respOrders[i].ID, Amount: respOrders[i].Size, ExecutedAmount: respOrders[i].FilledSize, - OrderType: orderType, - OrderDate: orderDate, - OrderSide: orderSide, - CurrencyPair: curr, + Type: orderType, + Date: orderDate, + Side: orderSide, + Pair: curr, Exchange: c.Name, }) } - order.FilterOrdersByType(&orders, req.OrderType) + order.FilterOrdersByType(&orders, req.Type) order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) - order.FilterOrdersBySide(&orders, req.OrderSide) + order.FilterOrdersBySide(&orders, req.Side) return orders, nil } diff --git a/exchanges/coinbene/coinbene_test.go b/exchanges/coinbene/coinbene_test.go index b740401e..c03a3382 100644 --- a/exchanges/coinbene/coinbene_test.go +++ b/exchanges/coinbene/coinbene_test.go @@ -9,6 +9,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/order" + "github.com/thrasher-corp/gocryptotrader/exchanges/sharedtestvalues" ) // Please supply your own keys here for due diligence testing @@ -42,7 +43,8 @@ func TestMain(m *testing.M) { if err != nil { log.Fatal(err) } - + c.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride() + c.Websocket.TrafficAlert = sharedtestvalues.GetWebsocketStructChannelOverride() os.Exit(m.Run()) } @@ -449,3 +451,233 @@ func TestGetSwapFundingRates(t *testing.T) { t.Error(err) } } + +func TestWsSubscribe(t *testing.T) { + pressXToJSON := []byte(`{"event":"subscribe","topic":"orderBook.BTCUSDT.10"}`) + err := c.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsUnsubscribe(t *testing.T) { + pressXToJSON := []byte(`{"event":"unsubscribe","topic":"tradeList.BTCUSDT"}`) + err := c.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsLogin(t *testing.T) { + pressXToJSON := []byte(`{"event":"login","success":true}`) + err := c.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } + + pressXToJSON = []byte(`{"event":"login","success":false}`) + err = c.wsHandleData(pressXToJSON) + if err == nil { + t.Error("Expected error") + } +} + +func TestWsOrderbook(t *testing.T) { + pressXToJSON := []byte(`{ + "topic": "orderBook.BTCUSDT", + "action": "insert", + "data": [{ + "asks": [ + ["5621.7", "58", "2"], + ["5621.8", "125", "5"], + ["5621.9", "100", "9"], + ["5622", "84", "20"], + ["5623.5", "90", "12"], + ["5624.2", "1540", "15"], + ["5625.1", "300", "20"], + ["5625.9", "350", "1"], + ["5629.3", "200", "1"], + ["5650", "1000", "8"] + ], + "bids": [ + ["5621.3", "287","8"], + ["5621.2", "41","1"], + ["5621.1", "2","1"], + ["5621", "26","2"], + ["5620.8", "194","2"], + ["5620", "2", "1"], + ["5618.8", "204","2"], + ["5618.4", "30", "9"], + ["5617.2", "2","1"], + ["5609.9", "100", "12"] + ], + "version":1, + "timestamp": "2019-07-04T02:21:08Z" + }] + }`) + err := c.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } + + pressXToJSON = []byte(`{ + "topic": "orderBook.BTCUSDT", + "action": "update", + "data": [{ + "asks": [ + ["5621.7", "50", "2"], + ["5621.8", "0", "0"], + ["5621.9", "30", "5"] + ], + "bids": [ + ["5621.3", "10","1"], + ["5621.2", "20","1"], + ["5621.1", "80","5"], + ["5621", "0","0"], + ["5620.8", "10","1"] + ], + "version":2, + "timestamp": "2019-07-04T02:21:09Z" + }] + }`) + err = c.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsTrade(t *testing.T) { + pressXToJSON := []byte(`{ + "topic": "tradeList.BTCUSDT", + "data": [ + [ + "8600.0000", + "s", + "100", + "2019-05-21T08:25:22.735Z" + ] + ] + }`) + err := c.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsTicker(t *testing.T) { + pressXToJSON := []byte(`{ + "topic": "ticker.BTCUSDT", + "data": [ + { + "symbol": "BTCUSDT", + "lastPrice": "8548.0", + "markPrice": "8548.0", + "bestAskPrice": "8601.0", + "bestBidPrice": "8600.0", + "bestAskVolume": "1222", + "bestBidVolume": "56505", + "high24h": "8600.0000", + "low24h": "242.4500", + "volume24h": "4994", + "timestamp": "2019-05-06T06:45:56.716Z" + } + ] + }`) + err := c.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsKLine(t *testing.T) { + pressXToJSON := []byte(`{ + "topic": "kline.BTCUSDT", + "data": [ + [ + "BTCUSDT", + 1557428280, + "5794", + "5794", + "5794", + "5794", + "0", + "0", + "0", + "0" + ] + ] + }`) + err := c.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsUserAccount(t *testing.T) { + pressXToJSON := []byte(`{ + "topic": "user.account", + "data": [{ + "asset": "BTC", + "availableBalance": "20.3859", + "frozenBalance": "0.7413", + "balance": "21.1272", + "timestamp": "2019-05-22T03:11:22.0Z" + }] +}`) + err := c.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsUserPosition(t *testing.T) { + pressXToJSON := []byte(`{ + "topic": "user.position", + "data": [{ + "availableQuantity": "100", + "avgPrice": "7778.1", + "leverage": "20", + "liquidationPrice": "5441.0", + "markPrice": "8086.5", + "positionMargin": "0.0285", + "quantity": "507", + "realisedPnl": "0.0069", + "side": "long", + "symbol": "BTCUSDT", + "marginMode": "1", + "createTime": "2019-05-22T03:11:22.0Z" + }] +}`) + err := c.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsUserOrder(t *testing.T) { + pressXToJSON := []byte(`{ + "topic": "user.order", + "data": [{ + "orderId": "580721369818955776", + "direction": "openLong", + "leverage": "20", + "symbol": "BTCUSDT", + "orderType": "limit", + "quantity": "7", + "orderPrice": "146.30", + "orderValue": "0.0010", + "fee": "0.0000", + "filledQuantity": "0", + "averagePrice": "0.00", + "orderTime": "2019-05-22T03:39:24.0Z", + "status": "new", + "lastFillQuantity": "0", + "lastFillPrice": "0", + "lastFillTime": "" + }] +}`) + err := c.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} diff --git a/exchanges/coinbene/coinbene_types.go b/exchanges/coinbene/coinbene_types.go index 93171667..fc979b27 100644 --- a/exchanges/coinbene/coinbene_types.go +++ b/exchanges/coinbene/coinbene_types.go @@ -85,20 +85,20 @@ type CancelOrdersResponse struct { // OrderInfo stores order info type OrderInfo struct { - OrderID string `json:"orderId"` - BaseAsset string `json:"baseAsset"` - QuoteAsset string `json:"quoteAsset"` - OrderType string `json:"orderDirection"` - Quantity float64 `json:"quntity,string"` - Amount float64 `json:"amout,string"` - FilledAmount float64 `json:"filledAmount"` - TakerRate float64 `json:"takerFeeRate,string"` - MakerRate float64 `json:"makerFeeRate,string"` - AvgPrice float64 `json:"avgPrice,string"` - OrderPrice float64 `json:"orderPrice,string"` - OrderStatus string `json:"orderStatus"` - OrderTime string `json:"orderTime"` - TotalFee float64 `json:"totalFee"` + OrderID string `json:"orderId"` + BaseAsset string `json:"baseAsset"` + QuoteAsset string `json:"quoteAsset"` + OrderType string `json:"orderDirection"` + Quantity float64 `json:"quntity,string"` + Amount float64 `json:"amout,string"` + FilledAmount float64 `json:"filledAmount"` + TakerRate float64 `json:"takerFeeRate,string"` + MakerRate float64 `json:"makerFeeRate,string"` + AvgPrice float64 `json:"avgPrice,string"` + OrderPrice float64 `json:"orderPrice,string"` + OrderStatus string `json:"orderStatus"` + OrderTime time.Time `json:"orderTime"` + TotalFee float64 `json:"totalFee"` } // OrderFills stores the fill info @@ -157,9 +157,9 @@ type WsKline struct { // WsUserData stores websocket user data type WsUserData struct { Asset string `json:"string"` - Available float64 `json:"availableBalance"` - Locked float64 `json:"frozenBalance"` - Total float64 `json:"balance"` + Available float64 `json:"availableBalance,string"` + Locked float64 `json:"frozenBalance,string"` + Total float64 `json:"balance,string"` Timestamp time.Time `json:"timestamp"` } @@ -171,17 +171,17 @@ type WsUserInfo struct { // WsPositionData stores websocket info on user's position type WsPositionData struct { - AvailableQuantity float64 `json:"availableQuantity"` - AveragePrice float64 `json:"avgPrice"` - Leverage float64 `json:"leverage"` - LiquidationPrice float64 `json:"liquidationPrice"` - MarkPrice float64 `json:"markPrice"` - PositionMargin float64 `json:"positionMargin"` - Quantity float64 `json:"quantity"` - RealisedPNL float64 `json:"realisedPnl"` + AvailableQuantity float64 `json:"availableQuantity,string"` + AveragePrice float64 `json:"avgPrice,string"` + Leverage int64 `json:"leverage,string"` + LiquidationPrice float64 `json:"liquidationPrice,string"` + MarkPrice float64 `json:"markPrice,string"` + PositionMargin float64 `json:"positionMargin,string"` + Quantity float64 `json:"quantity,string"` + RealisedPNL float64 `json:"realisedPnl,string"` Side string `json:"side"` Symbol string `json:"symbol"` - MarginMode int64 `json:"marginMode"` + MarginMode int64 `json:"marginMode,string"` CreateTime time.Time `json:"createTime"` } @@ -195,20 +195,20 @@ type WsPosition struct { type WsOrderData struct { OrderID string `json:"orderId"` Direction string `json:"direction"` - Leverage float64 `json:"leverage"` + Leverage int64 `json:"leverage,string"` Symbol string `json:"symbol"` OrderType string `json:"orderType"` - Quantity float64 `json:"quantity"` - OrderPrice float64 `json:"orderPrice"` - OrderValue float64 `json:"orderValue"` - Fee float64 `json:"fee"` - FilledQuantity float64 `json:"filledQuantity"` - AveragePrice float64 `json:"averagePrice"` + Quantity float64 `json:"quantity,string"` + OrderPrice float64 `json:"orderPrice,string"` + OrderValue float64 `json:"orderValue,string"` + Fee float64 `json:"fee,string"` + FilledQuantity float64 `json:"filledQuantity,string"` + AveragePrice float64 `json:"averagePrice,string"` OrderTime time.Time `json:"orderTime"` Status string `json:"status"` - LastFillQuantity float64 `json:"lastFillQuantity"` - LastFillPrice float64 `json:"lastFillPrice"` - LastFillTime time.Time `json:"lastFillTime"` + LastFillQuantity float64 `json:"lastFillQuantity,string"` + LastFillPrice float64 `json:"lastFillPrice,string"` + LastFillTime string `json:"lastFillTime"` } // WsUserOrders stores websocket user orders' data @@ -264,12 +264,12 @@ type SwapTrades []SwapTrade // SwapAccountInfo returns the swap account balance info type SwapAccountInfo struct { - AvailableBalance float64 `json:"availableBalance,string"` - FrozenBalance float64 `json:"frozenBalance,string"` - MarginBalance float64 `json:"marginBalance,string"` - MarginRate float64 `json:"marginRate,string"` - Balance float64 `json:"balance,string"` - UnrealisedPNL float64 `json:"unrealisedPnl,string"` + AvailableBalance float64 `json:"availableBalance,string"` + FrozenBalance float64 `json:"frozenBalance,string"` + MarginBalance float64 `json:"marginBalance,string"` + MarginRate float64 `json:"marginRate,string"` + Balance float64 `json:"balance,string"` + UnrealisedProfitAndLoss float64 `json:"unrealisedPnl,string"` } // SwapPosition stores a single swap position's data @@ -277,8 +277,8 @@ type SwapPosition struct { AvailableQuantity float64 `json:"availableQuantity,string"` AveragePrice float64 `json:"averagePrice,string"` CreateTime time.Time `json:"createTime"` - DeleveragePercentile int `json:"deleveragePercentile,string"` - Leverage int `json:"leverage,string"` + DeleveragePercentile int64 `json:"deleveragePercentile,string"` + Leverage int64 `json:"leverage,string"` LiquidationPrice float64 `json:"liquidationPrice,string"` MarkPrice float64 `json:"markPrice,string"` PositionMargin float64 `json:"positionMargin,string"` @@ -303,9 +303,9 @@ type SwapPlaceOrderResponse struct { type SwapOrder struct { OrderID string `json:"orderId"` Direction string `json:"direction"` - Leverage int `json:"leverage,string"` + Leverage int64 `json:"leverage,string"` OrderType string `json:"orderType"` - Quantitity float64 `json:"quantity,string"` + Quantity float64 `json:"quantity,string"` OrderPrice float64 `json:"orderPrice,string"` OrderValue float64 `json:"orderValue,string"` Fee float64 `json:"fee"` diff --git a/exchanges/coinbene/coinbene_websocket.go b/exchanges/coinbene/coinbene_websocket.go index 1e8ddbcf..26e9359c 100644 --- a/exchanges/coinbene/coinbene_websocket.go +++ b/exchanges/coinbene/coinbene_websocket.go @@ -14,6 +14,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" @@ -21,11 +22,13 @@ import ( ) const ( - coinbeneWsURL = "wss://ws-contract.coinbene.vip/openapi/ws" + wsContractURL = "wss://ws-contract.coinbene.vip/openapi/ws" event = "event" topic = "topic" ) +var comms = make(chan wshandler.WebsocketResponse) + // WsConnect connects to websocket func (c *Coinbene) WsConnect() error { if !c.Websocket.IsEnabled() || !c.IsEnabled() { @@ -36,7 +39,8 @@ func (c *Coinbene) WsConnect() error { if err != nil { return err } - go c.WsDataHandler() + + go c.wsReadData() if c.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) { err = c.Login() if err != nil { @@ -78,266 +82,294 @@ func (c *Coinbene) GenerateAuthSubs() { c.Websocket.SubscribeToChannels(subscriptions) } -// WsDataHandler handles websocket data -func (c *Coinbene) WsDataHandler() { +// wsReadData receives and passes on websocket messages for processing +func (c *Coinbene) wsReadData() { c.Websocket.Wg.Add(1) - defer c.Websocket.Wg.Done() - for { select { case <-c.Websocket.ShutdownC: return - default: - stream, err := c.WebsocketConn.ReadMessage() + resp, err := c.WebsocketConn.ReadMessage() if err != nil { - c.Websocket.DataHandler <- err + c.Websocket.ReadMessageErrors <- err return } - c.Websocket.TrafficAlert <- struct{}{} - if string(stream.Raw) == wshandler.Ping { - err = c.WebsocketConn.SendRawMessage(websocket.TextMessage, []byte(wshandler.Pong)) - if err != nil { - c.Websocket.DataHandler <- err - } - continue - } - var result map[string]interface{} - err = json.Unmarshal(stream.Raw, &result) + err = c.wsHandleData(resp.Raw) if err != nil { c.Websocket.DataHandler <- err } - _, ok := result[event] - switch { - case ok && (result[event].(string) == "subscribe" || result[event].(string) == "unsubscribe"): - continue - case ok && result[event].(string) == "error": - c.Websocket.DataHandler <- fmt.Errorf("message: %s. code: %v", result["message"], result["code"]) - continue - } - if ok && strings.Contains(result[event].(string), "login") { - if result["success"].(bool) { - c.Websocket.SetCanUseAuthenticatedEndpoints(true) - c.GenerateAuthSubs() - continue - } - c.Websocket.SetCanUseAuthenticatedEndpoints(false) - c.Websocket.DataHandler <- fmt.Errorf("message: %s. code: %v", result["message"], result["code"]) - continue - } - switch { - case strings.Contains(result[topic].(string), "ticker"): - var wsTicker WsTicker - err = json.Unmarshal(stream.Raw, &wsTicker) - if err != nil { - c.Websocket.DataHandler <- err - continue - } - for x := range wsTicker.Data { - c.Websocket.DataHandler <- &ticker.Price{ - Volume: wsTicker.Data[x].Volume24h, - Last: wsTicker.Data[x].LastPrice, - High: wsTicker.Data[x].High24h, - Low: wsTicker.Data[x].Low24h, - Bid: wsTicker.Data[x].BestBidPrice, - Ask: wsTicker.Data[x].BestAskPrice, - Pair: currency.NewPairFromFormattedPairs(wsTicker.Data[x].Symbol, - c.GetEnabledPairs(asset.PerpetualSwap), - c.GetPairFormat(asset.PerpetualSwap, true)), - ExchangeName: c.Name, - AssetType: asset.PerpetualSwap, - LastUpdated: wsTicker.Data[x].Timestamp, - } - } - case strings.Contains(result[topic].(string), "tradeList"): - var tradeList WsTradeList - err = json.Unmarshal(stream.Raw, &tradeList) - if err != nil { - c.Websocket.DataHandler <- err - continue - } - var t time.Time - var price, amount float64 - t, err = time.Parse(time.RFC3339, tradeList.Data[0][3]) - if err != nil { - c.Websocket.DataHandler <- err - continue - } - price, err = strconv.ParseFloat(tradeList.Data[0][0], 64) - if err != nil { - c.Websocket.DataHandler <- err - continue - } - amount, err = strconv.ParseFloat(tradeList.Data[0][2], 64) - if err != nil { - c.Websocket.DataHandler <- err - continue - } - p := strings.Replace(tradeList.Topic, "tradeList.", "", 1) - c.Websocket.DataHandler <- wshandler.TradeData{ - CurrencyPair: currency.NewPairFromFormattedPairs(p, - c.GetEnabledPairs(asset.PerpetualSwap), - c.GetPairFormat(asset.PerpetualSwap, true)), - Timestamp: t, - Price: price, - Amount: amount, - Exchange: c.Name, - AssetType: asset.PerpetualSwap, - Side: tradeList.Data[0][1], - } - case strings.Contains(result[topic].(string), "orderBook"): - orderBook := struct { - Topic string `json:"topic"` - Action string `json:"action"` - Data []struct { - Bids [][]string `json:"bids"` - Asks [][]string `json:"asks"` - Version int64 `json:"version"` - Timestamp time.Time `json:"timestamp"` - } `json:"data"` - }{} - err = json.Unmarshal(stream.Raw, &orderBook) - if err != nil { - c.Websocket.DataHandler <- err - continue - } - p := strings.Replace(orderBook.Topic, "orderBook.", "", 1) - cp := currency.NewPairFromFormattedPairs(p, - c.GetEnabledPairs(asset.PerpetualSwap), - c.GetPairFormat(asset.PerpetualSwap, true)) - var amount, price float64 - var asks, bids []orderbook.Item - for i := range orderBook.Data[0].Asks { - amount, err = strconv.ParseFloat(orderBook.Data[0].Asks[i][1], 64) - if err != nil { - c.Websocket.DataHandler <- err - continue - } - price, err = strconv.ParseFloat(orderBook.Data[0].Asks[i][0], 64) - if err != nil { - c.Websocket.DataHandler <- err - continue - } - asks = append(asks, orderbook.Item{ - Amount: amount, - Price: price, - }) - } - for j := range orderBook.Data[0].Bids { - amount, err = strconv.ParseFloat(orderBook.Data[0].Bids[j][1], 64) - if err != nil { - c.Websocket.DataHandler <- err - continue - } - price, err = strconv.ParseFloat(orderBook.Data[0].Bids[j][0], 64) - if err != nil { - c.Websocket.DataHandler <- err - continue - } - bids = append(bids, orderbook.Item{ - Amount: amount, - Price: price, - }) - } - if orderBook.Action == "insert" { - var newOB orderbook.Base - newOB.Asks = asks - newOB.Bids = bids - newOB.AssetType = asset.PerpetualSwap - newOB.Pair = cp - newOB.ExchangeName = c.Name - newOB.LastUpdated = orderBook.Data[0].Timestamp - err = c.Websocket.Orderbook.LoadSnapshot(&newOB) - if err != nil { - c.Websocket.DataHandler <- err - continue - } - c.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{Pair: newOB.Pair, - Asset: asset.PerpetualSwap, - Exchange: c.Name, - } - } else if orderBook.Action == "update" { - newOB := wsorderbook.WebsocketOrderbookUpdate{ - Asks: asks, - Bids: bids, - Asset: asset.PerpetualSwap, - Pair: cp, - UpdateID: orderBook.Data[0].Version, - UpdateTime: orderBook.Data[0].Timestamp, - } - err = c.Websocket.Orderbook.Update(&newOB) - if err != nil { - c.Websocket.DataHandler <- err - continue - } - c.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{Pair: newOB.Pair, - Asset: asset.PerpetualSwap, - Exchange: c.Name, - } - } - case strings.Contains(result[topic].(string), "kline"): - var kline WsKline - var tempFloat float64 - var tempKline []float64 - err = json.Unmarshal(stream.Raw, &kline) - if err != nil { - c.Websocket.DataHandler <- err - continue - } - for x := 2; x < len(kline.Data[0]); x++ { - tempFloat, err = strconv.ParseFloat(kline.Data[0][x].(string), 64) - if err != nil { - c.Websocket.DataHandler <- err - continue - } - tempKline = append(tempKline, tempFloat) - } - p := currency.NewPairFromFormattedPairs(kline.Data[0][0].(string), - c.GetEnabledPairs(asset.PerpetualSwap), - c.GetPairFormat(asset.PerpetualSwap, true)) - c.Websocket.DataHandler <- wshandler.KlineData{ - Timestamp: time.Unix(int64(kline.Data[0][1].(float64)), 0), - Pair: p, - AssetType: asset.PerpetualSwap, - Exchange: c.Name, - OpenPrice: tempKline[0], - ClosePrice: tempKline[1], - HighPrice: tempKline[2], - LowPrice: tempKline[3], - Volume: tempKline[4], - } - case strings.Contains(result[topic].(string), "user.account"): - var userinfo WsUserInfo - err = json.Unmarshal(stream.Raw, &userinfo) - if err != nil { - c.Websocket.DataHandler <- err - continue - } - c.Websocket.DataHandler <- userinfo - case strings.Contains(result[topic].(string), "user.position"): - var position WsPosition - err = json.Unmarshal(stream.Raw, &position) - if err != nil { - c.Websocket.DataHandler <- err - continue - } - c.Websocket.DataHandler <- position - case strings.Contains(result[topic].(string), "user.order"): - var orders WsUserOrders - err = json.Unmarshal(stream.Raw, &orders) - if err != nil { - c.Websocket.DataHandler <- err - continue - } - c.Websocket.DataHandler <- orders - default: - c.Websocket.DataHandler <- fmt.Errorf("%s - unhandled response '%s'", c.Name, stream.Raw) - } } } } +func (c *Coinbene) wsHandleData(respRaw []byte) error { + c.Websocket.TrafficAlert <- struct{}{} + if string(respRaw) == wshandler.Ping { + err := c.WebsocketConn.SendRawMessage(websocket.TextMessage, []byte(wshandler.Pong)) + if err != nil { + return err + } + return nil + } + var result map[string]interface{} + err := json.Unmarshal(respRaw, &result) + if err != nil { + return err + } + _, ok := result[event] + switch { + case ok && (result[event].(string) == "subscribe" || result[event].(string) == "unsubscribe"): + return nil + case ok && result[event].(string) == "error": + return fmt.Errorf("message: %s. code: %v", result["message"], result["code"]) + } + if ok && strings.Contains(result[event].(string), "login") { + if result["success"].(bool) { + c.Websocket.SetCanUseAuthenticatedEndpoints(true) + c.GenerateAuthSubs() + return nil + } + c.Websocket.SetCanUseAuthenticatedEndpoints(false) + return fmt.Errorf("message: %s. code: %v", result["message"], result["code"]) + } + switch { + case strings.Contains(result[topic].(string), "ticker"): + var wsTicker WsTicker + err = json.Unmarshal(respRaw, &wsTicker) + if err != nil { + return err + } + for x := range wsTicker.Data { + c.Websocket.DataHandler <- &ticker.Price{ + Volume: wsTicker.Data[x].Volume24h, + Last: wsTicker.Data[x].LastPrice, + High: wsTicker.Data[x].High24h, + Low: wsTicker.Data[x].Low24h, + Bid: wsTicker.Data[x].BestBidPrice, + Ask: wsTicker.Data[x].BestAskPrice, + Pair: currency.NewPairFromFormattedPairs(wsTicker.Data[x].Symbol, + c.GetEnabledPairs(asset.PerpetualSwap), + c.GetPairFormat(asset.PerpetualSwap, true)), + ExchangeName: c.Name, + AssetType: asset.PerpetualSwap, + LastUpdated: wsTicker.Data[x].Timestamp, + } + } + case strings.Contains(result[topic].(string), "tradeList"): + var tradeList WsTradeList + err = json.Unmarshal(respRaw, &tradeList) + if err != nil { + return err + } + var t time.Time + var price, amount float64 + t, err = time.Parse(time.RFC3339, tradeList.Data[0][3]) + if err != nil { + return err + } + price, err = strconv.ParseFloat(tradeList.Data[0][0], 64) + if err != nil { + return err + } + amount, err = strconv.ParseFloat(tradeList.Data[0][2], 64) + if err != nil { + return err + } + p := strings.Replace(tradeList.Topic, "tradeList.", "", 1) + var tSide = order.Buy + if tradeList.Data[0][1] == "s" { + tSide = order.Sell + } + c.Websocket.DataHandler <- wshandler.TradeData{ + CurrencyPair: currency.NewPairFromFormattedPairs(p, + c.GetEnabledPairs(asset.PerpetualSwap), + c.GetPairFormat(asset.PerpetualSwap, true)), + Timestamp: t, + Price: price, + Amount: amount, + Exchange: c.Name, + AssetType: asset.PerpetualSwap, + Side: tSide, + } + case strings.Contains(result[topic].(string), "orderBook"): + orderBook := struct { + Topic string `json:"topic"` + Action string `json:"action"` + Data []struct { + Bids [][]string `json:"bids"` + Asks [][]string `json:"asks"` + Version int64 `json:"version"` + Timestamp time.Time `json:"timestamp"` + } `json:"data"` + }{} + err = json.Unmarshal(respRaw, &orderBook) + if err != nil { + return err + } + p := strings.Replace(orderBook.Topic, "orderBook.", "", 1) + cp := currency.NewPairFromFormattedPairs(p, + c.GetEnabledPairs(asset.PerpetualSwap), + c.GetPairFormat(asset.PerpetualSwap, true)) + var amount, price float64 + var asks, bids []orderbook.Item + for i := range orderBook.Data[0].Asks { + amount, err = strconv.ParseFloat(orderBook.Data[0].Asks[i][1], 64) + if err != nil { + return err + } + price, err = strconv.ParseFloat(orderBook.Data[0].Asks[i][0], 64) + if err != nil { + return err + } + asks = append(asks, orderbook.Item{ + Amount: amount, + Price: price, + }) + } + for j := range orderBook.Data[0].Bids { + amount, err = strconv.ParseFloat(orderBook.Data[0].Bids[j][1], 64) + if err != nil { + return err + } + price, err = strconv.ParseFloat(orderBook.Data[0].Bids[j][0], 64) + if err != nil { + return err + } + bids = append(bids, orderbook.Item{ + Amount: amount, + Price: price, + }) + } + if orderBook.Action == "insert" { + var newOB orderbook.Base + newOB.Asks = asks + newOB.Bids = bids + newOB.AssetType = asset.PerpetualSwap + newOB.Pair = cp + newOB.ExchangeName = c.Name + newOB.LastUpdated = orderBook.Data[0].Timestamp + err = c.Websocket.Orderbook.LoadSnapshot(&newOB) + if err != nil { + return err + } + c.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{Pair: newOB.Pair, + Asset: asset.PerpetualSwap, + Exchange: c.Name, + } + } else if orderBook.Action == "update" { + newOB := wsorderbook.WebsocketOrderbookUpdate{ + Asks: asks, + Bids: bids, + Asset: asset.PerpetualSwap, + Pair: cp, + UpdateID: orderBook.Data[0].Version, + UpdateTime: orderBook.Data[0].Timestamp, + } + err = c.Websocket.Orderbook.Update(&newOB) + if err != nil { + return err + } + c.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{Pair: newOB.Pair, + Asset: asset.PerpetualSwap, + Exchange: c.Name, + } + } + case strings.Contains(result[topic].(string), "kline"): + var kline WsKline + var tempFloat float64 + var tempKline []float64 + err = json.Unmarshal(respRaw, &kline) + if err != nil { + return err + } + for x := 2; x < len(kline.Data[0]); x++ { + tempFloat, err = strconv.ParseFloat(kline.Data[0][x].(string), 64) + if err != nil { + return err + } + tempKline = append(tempKline, tempFloat) + } + p := currency.NewPairFromFormattedPairs(kline.Data[0][0].(string), + c.GetEnabledPairs(asset.PerpetualSwap), + c.GetPairFormat(asset.PerpetualSwap, true)) + if tempKline == nil && len(tempKline) < 5 { + return errors.New(c.Name + " - received bad data ") + } + c.Websocket.DataHandler <- wshandler.KlineData{ + Timestamp: time.Unix(int64(kline.Data[0][1].(float64)), 0), + Pair: p, + AssetType: asset.PerpetualSwap, + Exchange: c.Name, + OpenPrice: tempKline[0], + ClosePrice: tempKline[1], + HighPrice: tempKline[2], + LowPrice: tempKline[3], + Volume: tempKline[4], + } + case strings.Contains(result[topic].(string), "user.account"): + var userInfo WsUserInfo + err = json.Unmarshal(respRaw, &userInfo) + if err != nil { + return err + } + c.Websocket.DataHandler <- userInfo + case strings.Contains(result[topic].(string), "user.position"): + var position WsPosition + err = json.Unmarshal(respRaw, &position) + if err != nil { + return err + } + c.Websocket.DataHandler <- position + case strings.Contains(result[topic].(string), "user.order"): + var orders WsUserOrders + err = json.Unmarshal(respRaw, &orders) + if err != nil { + return err + } + for i := range orders.Data { + oType, err := order.StringToOrderType(orders.Data[i].OrderType) + if err != nil { + c.Websocket.DataHandler <- order.ClassificationError{ + Exchange: c.Name, + OrderID: orders.Data[i].OrderID, + Err: err, + } + } + oStatus, err := order.StringToOrderStatus(orders.Data[i].Status) + if err != nil { + c.Websocket.DataHandler <- order.ClassificationError{ + Exchange: c.Name, + OrderID: orders.Data[i].OrderID, + Err: err, + } + } + c.Websocket.DataHandler <- &order.Detail{ + Price: orders.Data[i].OrderPrice, + Amount: orders.Data[i].Quantity, + ExecutedAmount: orders.Data[i].FilledQuantity, + RemainingAmount: orders.Data[i].Quantity - orders.Data[i].FilledQuantity, + Fee: orders.Data[i].Fee, + Exchange: c.Name, + ID: orders.Data[i].OrderID, + Type: oType, + Status: oStatus, + AssetType: asset.PerpetualSwap, + Date: orders.Data[i].OrderTime, + Leverage: strconv.FormatInt(orders.Data[i].Leverage, 10), + Pair: currency.NewPairFromFormattedPairs(orders.Data[i].Symbol, + c.GetEnabledPairs(asset.PerpetualSwap), + c.GetPairFormat(asset.PerpetualSwap, true)), + } + } + default: + c.Websocket.DataHandler <- wshandler.UnhandledMessageWarning{Message: c.Name + wshandler.UnhandledMessage + string(respRaw)} + return nil + } + return nil +} + // Subscribe sends a websocket message to receive data from the channel func (c *Coinbene) Subscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error { var sub WsSub diff --git a/exchanges/coinbene/coinbene_wrapper.go b/exchanges/coinbene/coinbene_wrapper.go index 1beed2ae..ca35196e 100644 --- a/exchanges/coinbene/coinbene_wrapper.go +++ b/exchanges/coinbene/coinbene_wrapper.go @@ -108,6 +108,8 @@ func (c *Coinbene) SetDefaults() { Subscribe: true, Unsubscribe: true, AuthenticatedEndpoints: true, + GetOrders: true, + GetOrder: true, }, WithdrawPermissions: exchange.NoFiatWithdrawals | exchange.WithdrawCryptoViaWebsiteOnly, @@ -122,7 +124,7 @@ func (c *Coinbene) SetDefaults() { c.API.Endpoints.URLDefault = coinbeneAPIURL c.API.Endpoints.URL = c.API.Endpoints.URLDefault - c.API.Endpoints.WebsocketURL = coinbeneWsURL + c.API.Endpoints.WebsocketURL = wsContractURL c.Websocket = wshandler.New() c.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit c.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout @@ -147,7 +149,7 @@ func (c *Coinbene) Setup(exch *config.ExchangeConfig) error { Verbose: exch.Verbose, AuthenticatedWebsocketAPISupport: exch.API.AuthenticatedWebsocketSupport, WebsocketTimeout: exch.WebsocketTrafficTimeout, - DefaultURL: coinbeneWsURL, + DefaultURL: wsContractURL, ExchangeName: exch.Name, RunningURL: exch.API.Endpoints.WebsocketURL, Connector: c.WsConnect, @@ -470,20 +472,20 @@ func (c *Coinbene) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) { return resp, err } - if s.OrderSide != order.Buy && s.OrderSide != order.Sell { + if s.Side != order.Buy && s.Side != order.Sell { return resp, fmt.Errorf("%s orderside is not supported by this exchange", - s.OrderSide) + s.Side) } - if s.OrderType != order.Limit { + if s.Type != order.Limit { return resp, fmt.Errorf("only limit order is supported by this exchange") } tempResp, err := c.PlaceSpotOrder(s.Price, s.Amount, c.FormatExchangeCurrency(s.Pair, asset.Spot).String(), - s.OrderSide.String(), - s.OrderType.String(), + s.Side.String(), + s.Type.String(), s.ClientID, 0) if err != nil { @@ -502,7 +504,7 @@ func (c *Coinbene) ModifyOrder(action *order.Modify) (string, error) { // CancelOrder cancels an order by its corresponding ID number func (c *Coinbene) CancelOrder(order *order.Cancel) error { - _, err := c.CancelSpotOrder(order.OrderID) + _, err := c.CancelSpotOrder(order.ID) return err } @@ -510,7 +512,7 @@ func (c *Coinbene) CancelOrder(order *order.Cancel) error { func (c *Coinbene) CancelAllOrders(orderCancellation *order.Cancel) (order.CancelAllResponse, error) { var resp order.CancelAllResponse orders, err := c.FetchOpenSpotOrders( - c.FormatExchangeCurrency(orderCancellation.CurrencyPair, + c.FormatExchangeCurrency(orderCancellation.Pair, asset.Spot).String(), ) if err != nil { @@ -536,18 +538,13 @@ func (c *Coinbene) GetOrderInfo(orderID string) (order.Detail, error) { if err != nil { return resp, err } - var t time.Time resp.Exchange = c.Name resp.ID = orderID - resp.CurrencyPair = currency.NewPairWithDelimiter(tempResp.BaseAsset, + resp.Pair = currency.NewPairWithDelimiter(tempResp.BaseAsset, "/", tempResp.QuoteAsset) - t, err = time.Parse(time.RFC3339, tempResp.OrderTime) - if err != nil { - return resp, err - } resp.Price = tempResp.OrderPrice - resp.OrderDate = t + resp.Date = tempResp.OrderTime resp.ExecutedAmount = tempResp.FilledAmount resp.Fee = tempResp.TotalFee return resp, nil @@ -583,13 +580,13 @@ func (c *Coinbene) GetWebsocket() (*wshandler.Websocket, error) { // GetActiveOrders retrieves any orders that are active/open func (c *Coinbene) GetActiveOrders(getOrdersRequest *order.GetOrdersRequest) ([]order.Detail, error) { - if len(getOrdersRequest.Currencies) == 0 { + if len(getOrdersRequest.Pairs) == 0 { allPairs, err := c.GetAllPairs() if err != nil { return nil, err } for a := range allPairs { - getOrdersRequest.Currencies = append(getOrdersRequest.Currencies, + getOrdersRequest.Pairs = append(getOrdersRequest.Pairs, currency.NewPairFromString(allPairs[a].Symbol)) } } @@ -597,11 +594,11 @@ func (c *Coinbene) GetActiveOrders(getOrdersRequest *order.GetOrdersRequest) ([] var err error var resp []order.Detail - for x := range getOrdersRequest.Currencies { + for x := range getOrdersRequest.Pairs { var tempData OrdersInfo tempData, err = c.FetchOpenSpotOrders( c.FormatExchangeCurrency( - getOrdersRequest.Currencies[x], + getOrdersRequest.Pairs[x], asset.Spot).String(), ) if err != nil { @@ -611,19 +608,12 @@ func (c *Coinbene) GetActiveOrders(getOrdersRequest *order.GetOrdersRequest) ([] for y := range tempData { var tempResp order.Detail tempResp.Exchange = c.Name - tempResp.CurrencyPair = getOrdersRequest.Currencies[x] - tempResp.OrderSide = order.Buy + tempResp.Pair = getOrdersRequest.Pairs[x] + tempResp.Side = order.Buy if strings.EqualFold(tempData[y].OrderType, order.Sell.String()) { - tempResp.OrderSide = order.Sell + tempResp.Side = order.Sell } - - var t time.Time - t, err = time.Parse(time.RFC3339, tempData[y].OrderTime) - if err != nil { - return nil, err - } - - tempResp.OrderDate = t + tempResp.Date = tempData[y].OrderTime tempResp.Status = order.Status(tempData[y].OrderStatus) tempResp.Price = tempData[y].OrderPrice tempResp.Amount = tempData[y].Amount @@ -639,13 +629,13 @@ func (c *Coinbene) GetActiveOrders(getOrdersRequest *order.GetOrdersRequest) ([] // GetOrderHistory retrieves account order information // Can Limit response to specific order status func (c *Coinbene) GetOrderHistory(getOrdersRequest *order.GetOrdersRequest) ([]order.Detail, error) { - if len(getOrdersRequest.Currencies) == 0 { + if len(getOrdersRequest.Pairs) == 0 { allPairs, err := c.GetAllPairs() if err != nil { return nil, err } for a := range allPairs { - getOrdersRequest.Currencies = append(getOrdersRequest.Currencies, + getOrdersRequest.Pairs = append(getOrdersRequest.Pairs, currency.NewPairFromString(allPairs[a].Symbol)) } } @@ -654,10 +644,10 @@ func (c *Coinbene) GetOrderHistory(getOrdersRequest *order.GetOrdersRequest) ([] var tempData OrdersInfo var err error - for x := range getOrdersRequest.Currencies { + for x := range getOrdersRequest.Pairs { tempData, err = c.FetchClosedOrders( c.FormatExchangeCurrency( - getOrdersRequest.Currencies[x], + getOrdersRequest.Pairs[x], asset.Spot).String(), "", ) @@ -668,19 +658,12 @@ func (c *Coinbene) GetOrderHistory(getOrdersRequest *order.GetOrdersRequest) ([] for y := range tempData { var tempResp order.Detail tempResp.Exchange = c.Name - tempResp.CurrencyPair = getOrdersRequest.Currencies[x] - tempResp.OrderSide = order.Buy + tempResp.Pair = getOrdersRequest.Pairs[x] + tempResp.Side = order.Buy if strings.EqualFold(tempData[y].OrderType, order.Sell.String()) { - tempResp.OrderSide = order.Sell + tempResp.Side = order.Sell } - - var t time.Time - t, err = time.Parse(time.RFC3339, tempData[y].OrderTime) - if err != nil { - return nil, err - } - - tempResp.OrderDate = t + tempResp.Date = tempData[y].OrderTime tempResp.Status = order.Status(tempData[y].OrderStatus) tempResp.Price = tempData[y].OrderPrice tempResp.Amount = tempData[y].Amount diff --git a/exchanges/coinut/coinut.go b/exchanges/coinut/coinut.go index c8e34c22..f8026592 100644 --- a/exchanges/coinut/coinut.go +++ b/exchanges/coinut/coinut.go @@ -69,7 +69,7 @@ func (c *COINUT) SeedInstruments() error { } for _, y := range i.Instruments { - c.instrumentMap.Seed(y[0].Base+y[0].Quote, y[0].InstID) + c.instrumentMap.Seed(y[0].Base+y[0].Quote, y[0].InstrumentID) } return nil } diff --git a/exchanges/coinut/coinut_test.go b/exchanges/coinut/coinut_test.go index 8d63ed54..6e95e983 100644 --- a/exchanges/coinut/coinut_test.go +++ b/exchanges/coinut/coinut_test.go @@ -35,21 +35,24 @@ func TestMain(m *testing.M) { if err != nil { log.Fatal("Coinut load config error", err) } - bConfig, err := cfg.GetExchangeConfig("COINUT") + coinutCfg, err := cfg.GetExchangeConfig("COINUT") if err != nil { log.Fatal("Coinut Setup() init error") } - bConfig.API.AuthenticatedSupport = true - bConfig.API.AuthenticatedWebsocketSupport = true - bConfig.API.Credentials.Key = apiKey - bConfig.API.Credentials.ClientID = clientID - err = c.Setup(bConfig) + coinutCfg.API.AuthenticatedSupport = true + coinutCfg.API.AuthenticatedWebsocketSupport = true + coinutCfg.API.Credentials.Key = apiKey + coinutCfg.API.Credentials.ClientID = clientID + err = c.Setup(coinutCfg) if err != nil { log.Fatal("Coinut setup error", err) } - - c.SeedInstruments() - + err = c.SeedInstruments() + if err != nil { + log.Fatal("Coinut setup error ", err) + } + c.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride() + c.Websocket.TrafficAlert = sharedtestvalues.GetWebsocketStructChannelOverride() os.Exit(m.Run()) } @@ -80,7 +83,7 @@ func setupWSTestAuth(t *testing.T) { } c.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride() c.Websocket.TrafficAlert = sharedtestvalues.GetWebsocketStructChannelOverride() - go c.WsHandleData() + go c.wsReadData() err = c.wsAuthenticate() if err != nil { t.Error(err) @@ -259,7 +262,7 @@ func TestFormatWithdrawPermissions(t *testing.T) { func TestGetActiveOrders(t *testing.T) { var getOrdersRequest = order.GetOrdersRequest{ - OrderType: order.AnyType, + Type: order.AnyType, } _, err := c.GetActiveOrders(&getOrdersRequest) if areTestAPIKeysSet() && err != nil { @@ -270,8 +273,8 @@ func TestGetActiveOrders(t *testing.T) { func TestGetOrderHistoryWrapper(t *testing.T) { setupWSTestAuth(t) var getOrdersRequest = order.GetOrdersRequest{ - OrderType: order.AnyType, - Currencies: []currency.Pair{currency.NewPair(currency.BTC, + Type: order.AnyType, + Pairs: []currency.Pair{currency.NewPair(currency.BTC, currency.USD)}, } @@ -297,11 +300,11 @@ func TestSubmitOrder(t *testing.T) { Base: currency.BTC, Quote: currency.USD, }, - OrderSide: order.Buy, - OrderType: order.Limit, - Price: 1, - Amount: 1, - ClientID: "123", + Side: order.Buy, + Type: order.Limit, + Price: 1, + Amount: 1, + ClientID: "123", } response, err := c.SubmitOrder(orderSubmission) if areTestAPIKeysSet() && (err != nil || !response.IsOrderPlaced) { @@ -317,10 +320,10 @@ func TestCancelExchangeOrder(t *testing.T) { } currencyPair := currency.NewPair(currency.BTC, currency.USD) var orderCancellation = &order.Cancel{ - OrderID: "1", + ID: "1", WalletAddress: core.BitcoinDonationAddress, AccountID: "1", - CurrencyPair: currencyPair, + Pair: currencyPair, } err := c.CancelOrder(orderCancellation) @@ -339,10 +342,10 @@ func TestCancelAllExchangeOrders(t *testing.T) { currencyPair := currency.NewPair(currency.LTC, currency.BTC) var orderCancellation = &order.Cancel{ - OrderID: "1", + ID: "1", WalletAddress: core.BitcoinDonationAddress, AccountID: "1", - CurrencyPair: currencyPair, + Pair: currencyPair, } resp, err := c.CancelAllOrders(orderCancellation) @@ -520,7 +523,7 @@ func TestWsAuthCancelOrdersWrapper(t *testing.T) { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } orderDetails := order.Cancel{ - CurrencyPair: currency.NewPair(currency.LTC, currency.BTC), + Pair: currency.NewPair(currency.LTC, currency.BTC), } _, err := c.CancelAllOrders(&orderDetails) if err != nil { @@ -649,3 +652,479 @@ func TestGetNonce(t *testing.T) { } } } + +func TestWsOrderbook(t *testing.T) { + pressXToJSON := []byte(`{ + "buy": + [ { "count": 7, "price": "750.00000000", "qty": "0.07000000" }, + { "count": 1, "price": "751.00000000", "qty": "0.01000000" }, + { "count": 1, "price": "751.34500000", "qty": "0.01000000" } ], + "sell": + [ { "count": 6, "price": "750.58100000", "qty": "0.06000000" }, + { "count": 1, "price": "750.58200000", "qty": "0.01000000" }, + { "count": 1, "price": "750.58300000", "qty": "0.01000000" } ], + "inst_id": 1, + "nonce": 704114, + "total_buy": "67.52345000", + "total_sell": "0.08000000", + "reply": "inst_order_book", + "status": [ "OK" ] +}`) + err := c.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } + + pressXToJSON = []byte(`{ "count": 7, + "inst_id": 1, + "price": "750.58100000", + "qty": "0.07000000", + "total_buy": "120.06412000", + "reply": "inst_order_book_update", + "side": "BUY", + "trans_id": 169384 +}`) + err = c.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsTicker(t *testing.T) { + pressXToJSON := []byte(`{ + "highest_buy": "750.58100000", + "inst_id": 1, + "last": "752.00000000", + "lowest_sell": "752.00000000", + "reply": "inst_tick", + "timestamp": 1481355058109705, + "trans_id": 170064, + "volume": "0.07650000", + "volume24": "56.07650000" +}`) + err := c.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsGetInstruments(t *testing.T) { + pressXToJSON := []byte(`{ + "SPOT":{ + "LTCBTC":[ + { + "base":"LTC", + "inst_id":1, + "decimal_places":5, + "quote":"BTC" + } + ], + "ETHBTC":[ + { + "quote":"BTC", + "base":"ETH", + "decimal_places":5, + "inst_id":2 + } + ] + }, + "nonce":39116, + "reply":"inst_list", + "status":[ + "OK" + ] +}`) + err := c.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } + if c.instrumentMap.LookupID("ETHBTC") != 2 { + t.Error("Expected id to load") + } +} + +func TestWsTrades(t *testing.T) { + pressXToJSON := []byte(`{ + "nonce": 450319, + "reply": "inst_trade", + "status": [ + "OK" + ], + "trades": [ + { + "price": "750.00000000", + "qty": "0.01000000", + "side": "BUY", + "timestamp": 1481193563288963, + "trans_id": 169514 + }, + { + "price": "750.00000000", + "qty": "0.01000000", + "side": "BUY", + "timestamp": 1481193345279104, + "trans_id": 169510 + }, + { + "price": "750.00000000", + "qty": "0.01000000", + "side": "BUY", + "timestamp": 1481193333272230, + "trans_id": 169506 + }, + { + "price": "750.00000000", + "qty": "0.01000000", + "side": "BUY", + "timestamp": 1481193007342874, + "trans_id": 169502 + }] +}`) + err := c.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } + + pressXToJSON = []byte(`{ + "inst_id": 1, + "price": "750.58300000", + "reply": "inst_trade_update", + "side": "BUY", + "timestamp": 0, + "trans_id": 169478 +}`) + err = c.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsLogin(t *testing.T) { + pressXToJSON := []byte(`{ + "api_key":"b46e658f-d4c4-433c-b032-093423b1aaa4", + "country":"NA", + "email":"tester@test.com", + "failed_times":0, + "lang":"en_US", + "nonce":829055, + "otp_enabled":false, + "products_enabled":[ + "SPOT", + "FUTURE", + "BINARY_OPTION", + "OPTION" + ], + "reply":"login", + "session_id":"f8833081-af69-4266-904d-eea088cdcc52", + "status":[ + "OK" + ], + "timezone":"Asia/Singapore", + "unverified_email":"", + "username":"test" +}`) + err := c.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsAccountBalance(t *testing.T) { + pressXToJSON := []byte(`{ + "nonce": 306254, + "status": [ + "OK" + ], + "BTC": "192.46630415", + "LTC": "6000.00000000", + "ETC": "800.00000000", + "ETH": "496.99938000", + "floating_pl": "0.00000000", + "initial_margin": "0.00000000", + "realized_pl": "0.00000000", + "maintenance_margin": "0.00000000", + "equity": "192.46630415", + "reply": "user_balance", + "trans_id": 15159032 +}`) + err := c.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsOrder(t *testing.T) { + pressXToJSON := []byte(`{ + "nonce":956475, + "status":[ + "OK" + ], + "order_id":1, + "open_qty": "0.01", + "inst_id": 490590, + "qty":"0.01", + "client_ord_id": 1345, + "order_price":"750.581", + "reply":"order_accepted", + "side":"SELL", + "trans_id":127303 + }`) + err := c.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } + + pressXToJSON = []byte(` { + "commission": { + "amount": "0.00799000", + "currency": "USD" + }, + "fill_price": "799.00000000", + "fill_qty": "0.01000000", + "nonce": 956475, + "order": { + "client_ord_id": 12345, + "inst_id": 490590, + "open_qty": "0.00000000", + "order_id": 721923, + "price": "748.00000000", + "qty": "0.01000000", + "side": "SELL", + "timestamp": 1482903034617491 + }, + "reply": "order_filled", + "status": [ + "OK" + ], + "timestamp": 1482903034617491, + "trans_id": 20859252 + }`) + err = c.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } + + pressXToJSON = []byte(` { + "nonce": 275825, + "status": [ + "OK" + ], + "order_id": 7171, + "open_qty": "100000.00000000", + "price": "750.60000000", + "inst_id": 490590, + "reasons": [ + "NOT_ENOUGH_BALANCE" + ], + "client_ord_id": 4, + "timestamp": 1482080535098689, + "reply": "order_rejected", + "qty": "100000.00000000", + "side": "BUY", + "trans_id": 3282993 +}`) + err = c.wsHandleData(pressXToJSON) + if err == nil { + t.Error("Expected not enough balance error") + } +} + +func TestWsOrders(t *testing.T) { + pressXToJSON := []byte(`[ + { + "nonce": 621701, + "status": [ + "OK" + ], + "order_id": 331, + "open_qty": "0.01000000", + "price": "750.58100000", + "inst_id": 490590, + "client_ord_id": 1345, + "timestamp": 1490713990542441, + "reply": "order_accepted", + "qty": "0.01000000", + "side": "SELL", + "trans_id": 15155495 + }, + { + "nonce": 621701, + "status": [ + "OK" + ], + "order_id": 332, + "open_qty": "0.01000000", + "price": "750.32100000", + "inst_id": 490590, + "client_ord_id": 50001346, + "timestamp": 1490713990542441, + "reply": "order_accepted", + "qty": "0.01000000", + "side": "BUY", + "trans_id": 15155497 + } +]`) + err := c.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsOpenOrders(t *testing.T) { + pressXToJSON := []byte(`{ + "nonce": 1234, + "reply": "user_open_orders", + "status": [ + "OK" + ], + "orders": [ + { + "order_id": 35, + "open_qty": "0.01000000", + "price": "750.58200000", + "inst_id": 490590, + "client_ord_id": 4, + "timestamp": 1481138766081720, + "qty": "0.01000000", + "side": "BUY" + }, + { + "order_id": 30, + "open_qty": "0.01000000", + "price": "750.58100000", + "inst_id": 490590, + "client_ord_id": 5, + "timestamp": 1481137697919617, + "qty": "0.01000000", + "side": "BUY" + } + ] +}`) + err := c.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsCancelOrder(t *testing.T) { + pressXToJSON := []byte(` { + "nonce": 547201, + "reply": "cancel_order", + "order_id": 1, + "client_ord_id": 13556, + "status": [ + "OK" + ] + }`) + err := c.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsCancelOrders(t *testing.T) { + pressXToJSON := []byte(`{ + "nonce": 547201, + "reply": "cancel_orders", + "status": [ + "OK" + ], + "results": [ + { + "order_id": 329, + "status": "OK", + "inst_id": 490590, + "client_ord_id": 13561 + }, + { + "order_id": 332, + "status": "OK", + "inst_id": 490590, + "client_ord_id": 13562 + } + ], + "trans_id": 15166063 +}`) + err := c.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsOrderHistory(t *testing.T) { + pressXToJSON := []byte(`{ + "nonce": 326181, + "reply": "trade_history", + "status": [ + "OK" + ], + "total_number": 261, + "trades": [ + { + "commission": { + "amount": "0.00000100", + "currency": "BTC" + }, + "order": { + "client_ord_id": 297125564, + "inst_id": 490590, + "open_qty": "0.00000000", + "order_id": 721327, + "price": "1.00000000", + "qty": "0.00100000", + "side": "SELL", + "timestamp": 1482490337560987 + }, + "fill_price": "1.00000000", + "fill_qty": "0.00100000", + "timestamp": 1482490337560987, + "trans_id": 10020695 + }, + { + "commission": { + "amount": "0.00000100", + "currency": "BTC" + }, + "order": { + "client_ord_id": 297118937, + "inst_id": 490590, + "open_qty": "0.00000000", + "order_id": 721326, + "price": "1.00000000", + "qty": "0.00100000", + "side": "SELL", + "timestamp": 1482490330557949 + }, + "fill_price": "1.00000000", + "fill_qty": "0.00100000", + "timestamp": 1482490330557949, + "trans_id": 10020514 + } + ] +}`) + err := c.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestStringToStatus(t *testing.T) { + type TestCases struct { + Case string + Quantity float64 + Result order.Status + } + testCases := []TestCases{ + {Case: "order_accepted", Result: order.Active}, + {Case: "order_filled", Quantity: 1, Result: order.PartiallyFilled}, + {Case: "order_rejected", Result: order.Rejected}, + {Case: "order_filled", Result: order.Filled}, + {Case: "LOL", Result: order.UnknownStatus}, + } + for i := range testCases { + result, _ := stringToOrderStatus(testCases[i].Case, testCases[i].Quantity) + if result != testCases[i].Result { + t.Errorf("Exepcted: %v, received: %v", testCases[i].Result, result) + } + } +} diff --git a/exchanges/coinut/coinut_types.go b/exchanges/coinut/coinut_types.go index a647afdf..a4a6d148 100644 --- a/exchanges/coinut/coinut_types.go +++ b/exchanges/coinut/coinut_types.go @@ -9,17 +9,17 @@ import ( // GenericResponse is the generic response you will get from coinut type GenericResponse struct { - Nonce int64 `json:"nonce"` - Reply string `json:"reply"` - Status []string `json:"status"` - TransID int64 `json:"trans_id"` + Nonce int64 `json:"nonce"` + Reply string `json:"reply"` + Status []string `json:"status"` + TransactionID int64 `json:"trans_id"` } // InstrumentBase holds information on base currency type InstrumentBase struct { Base string `json:"base"` DecimalPlaces int `json:"decimal_places"` - InstID int64 `json:"inst_id"` + InstrumentID int64 `json:"inst_id"` Quote string `json:"quote"` } @@ -30,22 +30,22 @@ type Instruments struct { // Ticker holds ticker information type Ticker struct { - 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"` + 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"` + PreviousTransactionID 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"` + TransactionID 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 @@ -57,21 +57,21 @@ type OrderbookBase struct { // Orderbook is the full order book type Orderbook struct { - Buy []OrderbookBase `json:"buy"` - Sell []OrderbookBase `json:"sell"` - InstrumentID int `json:"inst_id"` - TotalBuy float64 `json:"total_buy,string"` - TotalSell float64 `json:"total_sell,string"` - TransID int64 `json:"trans_id"` + Buy []OrderbookBase `json:"buy"` + Sell []OrderbookBase `json:"sell"` + InstrumentID int `json:"inst_id"` + TotalBuy float64 `json:"total_buy,string"` + TotalSell float64 `json:"total_sell,string"` + TransactionID int64 `json:"trans_id"` } // TradeBase is a sub-type holding information on trades type TradeBase struct { - Price float64 `json:"price,string"` - Quantity float64 `json:"quantity,string"` - Side string `json:"side"` - Timestamp float64 `json:"timestamp"` - TransID int64 `json:"trans_id"` + Price float64 `json:"price,string"` + Quantity float64 `json:"quantity,string"` + Side string `json:"side"` + Timestamp float64 `json:"timestamp"` + TransactionID int64 `json:"trans_id"` } // Trades holds the full amount of trades associated with API keys @@ -152,11 +152,11 @@ type OrdersBase struct { // GetOpenOrdersResponse holds all order data from GetOpenOrders request type GetOpenOrdersResponse struct { - Nonce int `json:"nonce"` - Orders []OrderResponse `json:"orders"` - Reply string `json:"reply"` - Status []string `json:"status"` - TransID int `json:"trans_id"` + Nonce int `json:"nonce"` + Orders []OrderResponse `json:"orders"` + Reply string `json:"reply"` + Status []string `json:"status"` + TransactionID int `json:"trans_id"` } // OrdersResponse holds the full data range on orders @@ -268,12 +268,12 @@ type OpenPosition struct { } type wsRequest struct { - Request string `json:"request"` - SecType string `json:"sec_type,omitempty"` - InstID int64 `json:"inst_id,omitempty"` - TopN int64 `json:"top_n,omitempty"` - Subscribe bool `json:"subscribe,omitempty"` - Nonce int64 `json:"nonce,omitempty"` + Request string `json:"request"` + SecurityType string `json:"sec_type,omitempty"` + InstrumentID int64 `json:"inst_id,omitempty"` + TopN int64 `json:"top_n,omitempty"` + Subscribe bool `json:"subscribe,omitempty"` + Nonce int64 `json:"nonce,omitempty"` } type wsResponse struct { @@ -419,39 +419,39 @@ type WsCancelOrderParameters struct { // WsCancelOrderRequest data required for cancelling an order type WsCancelOrderRequest struct { - InstID int64 `json:"inst_id"` - OrderID int64 `json:"order_id"` + InstrumentID int64 `json:"inst_id"` + OrderID int64 `json:"order_id"` WsRequest } // WsCancelOrderResponse contains cancelled order data type WsCancelOrderResponse struct { - Nonce int64 `json:"nonce"` - Reply string `json:"reply"` - OrderID int64 `json:"order_id"` - ClientOrdID int64 `json:"client_ord_id"` - Status []string `json:"status"` + Nonce int64 `json:"nonce"` + Reply string `json:"reply"` + OrderID int64 `json:"order_id"` + ClientOrderID int64 `json:"client_ord_id"` + Status []string `json:"status"` } // WsCancelOrdersResponse contains all cancelled order data type WsCancelOrdersResponse struct { - Nonce int64 `json:"nonce"` - Reply string `json:"reply"` - Results []WsCancelOrdersResponseData `json:"results"` - Status []string `json:"status"` - TransID int64 `json:"trans_id"` + Nonce int64 `json:"nonce"` + Reply string `json:"reply"` + Results []WsCancelOrdersResponseData `json:"results"` + Status []string `json:"status"` + TransactionID int64 `json:"trans_id"` } // WsCancelOrdersResponseData individual cancellation response data type WsCancelOrdersResponseData struct { - InstID int64 `json:"inst_id"` - OrderID int64 `json:"order_id"` - Status string `json:"status"` + InstrumentID int64 `json:"inst_id"` + OrderID int64 `json:"order_id"` + Status string `json:"status"` } // WsGetOpenOrdersRequest ws request type WsGetOpenOrdersRequest struct { - InstID int64 `json:"inst_id"` + InstrumentID int64 `json:"inst_id"` WsRequest } @@ -463,20 +463,20 @@ type WsSubmitOrdersRequest struct { // WsSubmitOrdersRequestData ws request data type WsSubmitOrdersRequestData struct { - InstID int64 `json:"inst_id"` - Price float64 `json:"price,string"` - Qty float64 `json:"qty,string"` - ClientOrdID int `json:"client_ord_id"` - Side string `json:"side"` + InstrumentID int64 `json:"inst_id"` + Price float64 `json:"price,string"` + Quantity float64 `json:"qty,string"` + ClientOrderID int `json:"client_ord_id"` + Side string `json:"side"` } // WsSubmitOrderRequest ws request type WsSubmitOrderRequest struct { - InstID int64 `json:"inst_id"` - Price float64 `json:"price,string"` - Qty float64 `json:"qty,string"` - OrderID int64 `json:"client_ord_id"` - Side string `json:"side"` + InstrumentID int64 `json:"inst_id"` + Price float64 `json:"price,string"` + Quantity float64 `json:"qty,string"` + OrderID int64 `json:"client_ord_id"` + Side string `json:"side"` WsRequest } @@ -490,60 +490,60 @@ type WsSubmitOrderParameters struct { // WsUserBalanceResponse ws response type WsUserBalanceResponse struct { - Nonce int64 `json:"nonce"` - Status []string `json:"status"` - Btc float64 `json:"BTC,string"` - Ltc float64 `json:"LTC,string"` - Etc float64 `json:"ETC,string"` - Eth float64 `json:"ETH,string"` - FloatingPl float64 `json:"floating_pl,string"` - InitialMargin float64 `json:"initial_margin,string"` - RealizedPl float64 `json:"realized_pl,string"` - MaintenanceMargin float64 `json:"maintenance_margin,string"` - Equity float64 `json:"equity,string"` - Reply string `json:"reply"` - TransID int64 `json:"trans_id"` + Nonce int64 `json:"nonce"` + Status []string `json:"status"` + Btc float64 `json:"BTC,string"` + Ltc float64 `json:"LTC,string"` + Etc float64 `json:"ETC,string"` + Eth float64 `json:"ETH,string"` + FloatingProfitLoss float64 `json:"floating_pl,string"` + InitialMargin float64 `json:"initial_margin,string"` + RealisedProfitLoss float64 `json:"realized_pl,string"` + MaintenanceMargin float64 `json:"maintenance_margin,string"` + Equity float64 `json:"equity,string"` + Reply string `json:"reply"` + TransactionID int64 `json:"trans_id"` } // WsOrderAcceptedResponse ws response type WsOrderAcceptedResponse struct { - Nonce int64 `json:"nonce"` - Status []string `json:"status"` - OrderID int64 `json:"order_id"` - OpenQty float64 `json:"open_qty,string"` - InstID int64 `json:"inst_id"` - Qty float64 `json:"qty,string"` - ClientOrdID int64 `json:"client_ord_id"` - OrderPrice float64 `json:"order_price,string"` - Reply string `json:"reply"` - Side string `json:"side"` - TransID int64 `json:"trans_id"` + Nonce int64 `json:"nonce"` + Status []string `json:"status"` + OrderID int64 `json:"order_id"` + OpenQuantity float64 `json:"open_qty,string"` + InstrumentID int64 `json:"inst_id"` + Quantity float64 `json:"qty,string"` + ClientOrderID int64 `json:"client_ord_id"` + OrderPrice float64 `json:"order_price,string"` + Reply string `json:"reply"` + Side string `json:"side"` + TransactionID int64 `json:"trans_id"` } // WsOrderFilledResponse ws response type WsOrderFilledResponse struct { - Commission WsOrderFilledCommissionData `json:"commission"` - FillPrice float64 `json:"fill_price,string"` - FillQty float64 `json:"fill_qty,string"` - Nonce int64 `json:"nonce"` - Order WsOrderData `json:"order"` - Reply string `json:"reply"` - Status []string `json:"status"` - Timestamp int64 `json:"timestamp"` - TransID int64 `json:"trans_id"` + Commission WsOrderFilledCommissionData `json:"commission"` + FillPrice float64 `json:"fill_price,string"` + FillQuantity float64 `json:"fill_qty,string"` + Nonce int64 `json:"nonce"` + Order WsOrderData `json:"order"` + Reply string `json:"reply"` + Status []string `json:"status"` + Timestamp int64 `json:"timestamp"` + TransactionID int64 `json:"trans_id"` } // WsOrderData ws response data type WsOrderData struct { - ClientOrdID int64 `json:"client_ord_id"` - InstID int64 `json:"inst_id"` - OpenQty float64 `json:"open_qty,string"` - OrderID int64 `json:"order_id"` - Price float64 `json:"price,string"` - Qty float64 `json:"qty,string"` - Side string `json:"side"` - Timestamp int64 `json:"timestamp"` - Status []string `json:"status"` + ClientOrderID int64 `json:"client_ord_id"` + InstrumentID int64 `json:"inst_id"` + OpenQuantity float64 `json:"open_qty,string"` + OrderID int64 `json:"order_id"` + Price float64 `json:"price,string"` + Quantity float64 `json:"qty,string"` + Side string `json:"side"` + Timestamp int64 `json:"timestamp"` + Status []string `json:"status"` } // WsOrderFilledCommissionData ws response data @@ -554,38 +554,28 @@ type WsOrderFilledCommissionData struct { // WsOrderRejectedResponse ws response type WsOrderRejectedResponse struct { - Nonce int64 `json:"nonce"` - Status []string `json:"status"` - OrderID int64 `json:"order_id"` - OpenQty float64 `json:"open_qty,string"` - Price float64 `json:"price,string"` - InstID int64 `json:"inst_id"` - Reasons []string `json:"reasons"` - ClientOrdID int64 `json:"client_ord_id"` - Timestamp int64 `json:"timestamp"` - Reply string `json:"reply"` - Qty float64 `json:"qty,string"` - Side string `json:"side"` - TransID int64 `json:"trans_id"` + Nonce int64 `json:"nonce"` + Status []string `json:"status"` + OrderID int64 `json:"order_id"` + OpenQuantity float64 `json:"open_qty,string"` + Price float64 `json:"price,string"` + InstrumentID int64 `json:"inst_id"` + Reasons []string `json:"reasons"` + ClientOrderID int64 `json:"client_ord_id"` + Timestamp int64 `json:"timestamp"` + Reply string `json:"reply"` + Quantity float64 `json:"qty,string"` + Side string `json:"side"` + TransactionID int64 `json:"trans_id"` } -// WsStandardOrderResponse a standardised order -type WsStandardOrderResponse struct { - InstID int64 - OrderID int64 - ClientOrdID int64 - TransID int64 - Nonce int64 - Status []string - Qty float64 - OpenQty float64 - Price float64 - Side string - Reasons []string - Timestamp int64 - OrderType string - CommissionAmount float64 - CommissionCurrency currency.Pair +type wsInstList struct { + Spot map[string][]struct { + Base string `json:"base"` + DecimalPlaces int64 `json:"decimal_places"` + InstrumentID int64 `json:"inst_id"` + Quote string `json:"quote"` + } `json:"spot"` } // WsUserOpenOrdersResponse ws response @@ -613,12 +603,12 @@ type WsTradeHistoryCommissionData struct { // WsTradeHistoryTradeData ws response data type WsTradeHistoryTradeData struct { - Commission WsTradeHistoryCommissionData `json:"commission"` - Order WsOrderData `json:"order"` - FillPrice float64 `json:"fill_price,string"` - FillQty float64 `json:"fill_qty,string"` - Timestamp int64 `json:"timestamp"` - TransID int64 `json:"trans_id"` + Commission WsTradeHistoryCommissionData `json:"commission"` + Order WsOrderData `json:"order"` + FillPrice float64 `json:"fill_price,string"` + FillQuantity float64 `json:"fill_qty,string"` + Timestamp int64 `json:"timestamp"` + TransactionID int64 `json:"trans_id"` } // WsLoginResponse ws response data @@ -630,9 +620,9 @@ type WsLoginResponse struct { Email string `json:"email"` FailedTimes int64 `json:"failed_times"` KycPassed bool `json:"kyc_passed"` - Lang string `json:"lang"` + Language string `json:"lang"` Nonce int64 `json:"nonce"` - OtpEnabled bool `json:"otp_enabled"` + OTPEnabled bool `json:"otp_enabled"` PhoneNumber string `json:"phone_number"` ProductsEnabled []string `json:"products_enabled"` Referred bool `json:"referred"` @@ -648,10 +638,10 @@ type WsLoginResponse struct { // WsNewOrderResponse returns if new_order response failes type WsNewOrderResponse struct { - Msg string `json:"msg"` - Nonce int64 `json:"nonce"` - Reply string `json:"reply"` - Status []string `json:"status"` + Message string `json:"msg"` + Nonce int64 `json:"nonce"` + Reply string `json:"reply"` + Status []string `json:"status"` } // WsGetAccountBalanceResponse contains values of each currency @@ -681,3 +671,36 @@ type instrumentMap struct { Loaded bool m sync.Mutex } + +type wsOrderContainer struct { + OrderID int64 `json:"order_id"` + ClientOrderID int64 `json:"client_ord_id"` + InstrumentID int64 `json:"inst_id"` + Nonce int64 `json:"nonce"` + Timestamp int64 `json:"timestamp"` + TransactionID int64 `json:"trans_id"` + OpenQuantity float64 `json:"open_qty,string"` + OrderPrice float64 `json:"order_price,string"` + Quantity float64 `json:"qty,string"` + FillPrice float64 `json:"fill_price,string"` + FillQuantity float64 `json:"fill_qty,string"` + Price float64 `json:"price,string"` + Reply string `json:"reply"` + Side string `json:"side"` + Status []string `json:"status"` + Reasons []string `json:"reasons"` + Order struct { + ClientOrderID int64 `json:"client_ord_id"` + InstrumentID int64 `json:"inst_id"` + OrderID int64 `json:"order_id"` + Timestamp int64 `json:"timestamp"` + Price float64 `json:"price,string"` + Quantity float64 `json:"qty,string"` + OpenQuantity float64 `json:"open_qty,string"` + Side string `json:"side"` + } `json:"order"` + Commission struct { + Amount float64 `json:"amount,string"` + Currency string `json:"currency"` + } `json:"commission"` +} diff --git a/exchanges/coinut/coinut_websocket.go b/exchanges/coinut/coinut_websocket.go index 4b3e9757..bd271d12 100644 --- a/exchanges/coinut/coinut_websocket.go +++ b/exchanges/coinut/coinut_websocket.go @@ -46,7 +46,7 @@ func (c *COINUT) WsConnect() error { if err != nil { return err } - go c.WsHandleData() + go c.wsReadData() if !c.instrumentMap.IsLoaded() { _, err = c.WsGetInstruments() @@ -68,8 +68,8 @@ func (c *COINUT) WsConnect() error { return nil } -// WsHandleData handles read data -func (c *COINUT) WsHandleData() { +// wsReadData receives and passes on websocket messages for processing +func (c *COINUT) wsReadData() { c.Websocket.Wg.Add(1) defer func() { @@ -98,8 +98,10 @@ func (c *COINUT) WsHandleData() { } for i := range incoming { if incoming[i].Nonce > 0 { - c.WebsocketConn.AddResponseWithID(incoming[i].Nonce, resp.Raw) - break + if c.WebsocketConn.IsIDWaitingForResponse(incoming[i].Nonce) { + c.WebsocketConn.SetResponseIDAndData(incoming[i].Nonce, resp.Raw) + break + } } var individualJSON []byte individualJSON, err = json.Marshal(incoming[i]) @@ -107,7 +109,10 @@ func (c *COINUT) WsHandleData() { c.Websocket.DataHandler <- err continue } - c.wsProcessResponse(individualJSON) + err = c.wsHandleData(individualJSON) + if err != nil { + c.Websocket.DataHandler <- err + } } } else { var incoming wsResponse @@ -116,31 +121,116 @@ func (c *COINUT) WsHandleData() { c.Websocket.DataHandler <- err continue } - - c.wsProcessResponse(resp.Raw) + err = c.wsHandleData(resp.Raw) + if err != nil { + c.Websocket.DataHandler <- err + } } } } } -func (c *COINUT) wsProcessResponse(resp []byte) { +func (c *COINUT) wsHandleData(respRaw []byte) error { + if strings.HasPrefix(string(respRaw), "[") { + var orders []wsOrderContainer + err := json.Unmarshal(respRaw, &orders) + if err != nil { + return err + } + for i := range orders { + o, err2 := c.parseOrderContainer(&orders[i]) + if err2 != nil { + return err2 + } + c.Websocket.DataHandler <- o + } + return nil + } + var incoming wsResponse - err := json.Unmarshal(resp, &incoming) + err := json.Unmarshal(respRaw, &incoming) if err != nil { - c.Websocket.DataHandler <- err - return + return err + } + if strings.Contains(string(respRaw), "client_ord_id") { + if c.WebsocketConn.IsIDWaitingForResponse(incoming.Nonce) { + c.WebsocketConn.SetResponseIDAndData(incoming.Nonce, respRaw) + return nil + } } switch incoming.Reply { case "hb": - channels["hb"] <- resp + channels["hb"] <- respRaw + case "login": + var login WsLoginResponse + err := json.Unmarshal(respRaw, &login) + if err != nil { + return err + } + if login.APIKey != c.API.Credentials.Key { + c.API.AuthenticatedWebsocketSupport = false + } + case "user_balance": + var userBalance WsUserBalanceResponse + err := json.Unmarshal(respRaw, &userBalance) + if err != nil { + return err + } + case "user_open_orders": + var openOrders WsUserOpenOrdersResponse + err := json.Unmarshal(respRaw, &openOrders) + if err != nil { + return err + } + case "cancel_order": + var cancel WsCancelOrderResponse + err := json.Unmarshal(respRaw, &cancel) + if err != nil { + return err + } + c.Websocket.DataHandler <- &order.Modify{ + Exchange: c.Name, + ID: strconv.FormatInt(cancel.OrderID, 10), + Status: order.Cancelled, + LastUpdated: time.Now(), + } + case "cancel_orders": + var cancels WsCancelOrdersResponse + err := json.Unmarshal(respRaw, &cancels) + if err != nil { + return err + } + for i := range cancels.Results { + c.Websocket.DataHandler <- &order.Modify{ + Exchange: c.Name, + ID: strconv.FormatInt(cancels.Results[i].OrderID, 10), + Status: order.Cancelled, + LastUpdated: time.Now(), + } + } + case "trade_history": + var trades WsTradeHistoryResponse + err := json.Unmarshal(respRaw, &trades) + if err != nil { + return err + } + case "inst_list": + var instList wsInstList + err := json.Unmarshal(respRaw, &instList) + if err != nil { + return err + } + for k, v := range instList.Spot { + for _, v2 := range v { + c.instrumentMap.Seed(k, v2.InstrumentID) + } + } case "inst_tick": var wsTicker WsTicker - err := json.Unmarshal(resp, &wsTicker) + err := json.Unmarshal(respRaw, &wsTicker) if err != nil { - c.Websocket.DataHandler <- err - return + return err } - currencyPair := c.instrumentMap.LookupInstrument(wsTicker.InstID) c.Websocket.DataHandler <- &ticker.Price{ ExchangeName: c.Name, @@ -157,20 +247,17 @@ func (c *COINUT) wsProcessResponse(resp []byte) { c.GetEnabledPairs(asset.Spot), c.GetPairFormat(asset.Spot, true)), } - case "inst_order_book": - var orderbooksnapshot WsOrderbookSnapshot - err := json.Unmarshal(resp, &orderbooksnapshot) + var orderbookSnapshot WsOrderbookSnapshot + err := json.Unmarshal(respRaw, &orderbookSnapshot) if err != nil { - c.Websocket.DataHandler <- err - return + return err } - err = c.WsProcessOrderbookSnapshot(&orderbooksnapshot) + err = c.WsProcessOrderbookSnapshot(&orderbookSnapshot) if err != nil { - c.Websocket.DataHandler <- err - return + return err } - currencyPair := c.instrumentMap.LookupInstrument(orderbooksnapshot.InstID) + currencyPair := c.instrumentMap.LookupInstrument(orderbookSnapshot.InstID) c.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{ Exchange: c.Name, Asset: asset.Spot, @@ -180,15 +267,13 @@ func (c *COINUT) wsProcessResponse(resp []byte) { } case "inst_order_book_update": var orderbookUpdate WsOrderbookUpdate - err := json.Unmarshal(resp, &orderbookUpdate) + err := json.Unmarshal(respRaw, &orderbookUpdate) if err != nil { - c.Websocket.DataHandler <- err - return + return err } err = c.WsProcessOrderbookUpdate(&orderbookUpdate) if err != nil { - c.Websocket.DataHandler <- err - return + return err } currencyPair := c.instrumentMap.LookupInstrument(orderbookUpdate.InstID) c.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{ @@ -200,20 +285,25 @@ func (c *COINUT) wsProcessResponse(resp []byte) { } case "inst_trade": var tradeSnap WsTradeSnapshot - err := json.Unmarshal(resp, &tradeSnap) + err := json.Unmarshal(respRaw, &tradeSnap) if err != nil { - c.Websocket.DataHandler <- err - return + return err } case "inst_trade_update": var tradeUpdate WsTradeUpdate - err := json.Unmarshal(resp, &tradeUpdate) + err := json.Unmarshal(respRaw, &tradeUpdate) if err != nil { - c.Websocket.DataHandler <- err - return + return err } currencyPair := c.instrumentMap.LookupInstrument(tradeUpdate.InstID) + tSide, err := order.StringToOrderSide(tradeUpdate.Side) + if err != nil { + c.Websocket.DataHandler <- order.ClassificationError{ + Exchange: c.Name, + Err: err, + } + } c.Websocket.DataHandler <- wshandler.TradeData{ Timestamp: time.Unix(tradeUpdate.Timestamp, 0), CurrencyPair: currency.NewPairFromFormattedPairs(currencyPair, @@ -222,24 +312,137 @@ func (c *COINUT) wsProcessResponse(resp []byte) { AssetType: asset.Spot, Exchange: c.Name, Price: tradeUpdate.Price, - Side: tradeUpdate.Side, + Side: tSide, } + case "order_filled", "order_rejected", "order_accepted": + var orderContainer wsOrderContainer + err := json.Unmarshal(respRaw, &orderContainer) + if err != nil { + return err + } + o, err := c.parseOrderContainer(&orderContainer) + if err != nil { + return err + } + c.Websocket.DataHandler <- o default: - if incoming.Nonce > 0 { - c.WebsocketConn.AddResponseWithID(incoming.Nonce, resp) - return - } - c.Websocket.DataHandler <- fmt.Errorf("%v unhandled websocket response: %s", c.Name, resp) + c.Websocket.DataHandler <- wshandler.UnhandledMessageWarning{Message: c.Name + wshandler.UnhandledMessage + string(respRaw)} + return nil } + return nil +} + +func stringToOrderStatus(status string, quantity float64) (order.Status, error) { + switch status { + case "order_accepted": + return order.Active, nil + case "order_filled": + if quantity > 0 { + return order.PartiallyFilled, nil + } + return order.Filled, nil + case "order_rejected": + return order.Rejected, nil + default: + return order.UnknownStatus, errors.New(status + " not recognised as order status") + } +} + +func (c *COINUT) parseOrderContainer(oContainer *wsOrderContainer) (*order.Detail, error) { + var oSide order.Side + var oStatus order.Status + var err error + var orderID = strconv.FormatInt(oContainer.OrderID, 10) + if oContainer.Side != "" { + oSide, err = order.StringToOrderSide(oContainer.Side) + if err != nil { + c.Websocket.DataHandler <- order.ClassificationError{ + Exchange: c.Name, + OrderID: orderID, + Err: err, + } + } + } else if oContainer.Order.Side != "" { + oSide, err = order.StringToOrderSide(oContainer.Order.Side) + if err != nil { + c.Websocket.DataHandler <- order.ClassificationError{ + Exchange: c.Name, + OrderID: orderID, + Err: err, + } + } + } + + oStatus, err = stringToOrderStatus(oContainer.Reply, oContainer.OpenQuantity) + if err != nil { + c.Websocket.DataHandler <- order.ClassificationError{ + Exchange: c.Name, + OrderID: orderID, + Err: err, + } + } + if oContainer.Status[0] != "OK" { + return nil, fmt.Errorf("%s - Order rejected: %v", c.Name, oContainer.Status) + } + if len(oContainer.Reasons) > 0 { + return nil, fmt.Errorf("%s - Order rejected: %v", c.Name, oContainer.Reasons) + } + + o := &order.Detail{ + Price: oContainer.Price, + Amount: oContainer.Quantity, + ExecutedAmount: oContainer.FillQuantity, + RemainingAmount: oContainer.OpenQuantity, + Exchange: c.Name, + ID: orderID, + Side: oSide, + Status: oStatus, + Date: time.Unix(0, oContainer.Timestamp), + Trades: nil, + } + if oContainer.Reply == "order_filled" { + o.Side, err = order.StringToOrderSide(oContainer.Order.Side) + if err != nil { + c.Websocket.DataHandler <- order.ClassificationError{ + Exchange: c.Name, + OrderID: orderID, + Err: err, + } + } + o.RemainingAmount = oContainer.Order.OpenQuantity + o.Amount = oContainer.Order.Quantity + o.ID = strconv.FormatInt(oContainer.Order.OrderID, 10) + o.LastUpdated = time.Unix(0, oContainer.Timestamp) + o.Pair, o.AssetType, err = c.GetRequestFormattedPairAndAssetType(c.instrumentMap.LookupInstrument(oContainer.Order.InstrumentID)) + if err != nil { + return nil, err + } + o.Trades = []order.TradeHistory{ + { + Price: oContainer.FillPrice, + Amount: oContainer.FillQuantity, + Exchange: c.Name, + TID: strconv.FormatInt(oContainer.TransactionID, 10), + Side: oSide, + Timestamp: time.Unix(0, oContainer.Timestamp), + }, + } + } else { + o.Pair, o.AssetType, err = c.GetRequestFormattedPairAndAssetType(c.instrumentMap.LookupInstrument(oContainer.InstrumentID)) + if err != nil { + return nil, err + } + } + return o, nil } // WsGetInstruments fetches instrument list and propagates a local cache func (c *COINUT) WsGetInstruments() (Instruments, error) { var list Instruments request := wsRequest{ - Request: "inst_list", - SecType: strings.ToUpper(asset.Spot.String()), - Nonce: getNonce(), + Request: "inst_list", + SecurityType: strings.ToUpper(asset.Spot.String()), + Nonce: getNonce(), } resp, err := c.WebsocketConn.SendMessageReturnResponse(request.Nonce, request) if err != nil { @@ -250,7 +453,7 @@ func (c *COINUT) WsGetInstruments() (Instruments, error) { return list, err } for curr, data := range list.Instruments { - c.instrumentMap.Seed(curr, data[0].InstID) + c.instrumentMap.Seed(curr, data[0].InstrumentID) } if len(c.instrumentMap.GetInstrumentIDs()) == 0 { return list, errors.New("instrument list failed to populate") @@ -330,7 +533,7 @@ func (c *COINUT) GenerateDefaultSubscriptions() { func (c *COINUT) Subscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error { subscribe := wsRequest{ Request: channelToSubscribe.Channel, - InstID: c.instrumentMap.LookupID(c.FormatExchangeCurrency(channelToSubscribe.Currency, + InstrumentID: c.instrumentMap.LookupID(c.FormatExchangeCurrency(channelToSubscribe.Currency, asset.Spot).String()), Subscribe: true, Nonce: getNonce(), @@ -342,7 +545,7 @@ func (c *COINUT) Subscribe(channelToSubscribe wshandler.WebsocketChannelSubscrip func (c *COINUT) Unsubscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error { subscribe := wsRequest{ Request: channelToSubscribe.Channel, - InstID: c.instrumentMap.LookupID(c.FormatExchangeCurrency(channelToSubscribe.Currency, + InstrumentID: c.instrumentMap.LookupID(c.FormatExchangeCurrency(channelToSubscribe.Currency, asset.Spot).String()), Subscribe: false, Nonce: getNonce(), @@ -426,7 +629,7 @@ func (c *COINUT) wsGetAccountBalance() (*UserBalance, error) { return &response, nil } -func (c *COINUT) wsSubmitOrder(o *WsSubmitOrderParameters) (*WsStandardOrderResponse, error) { +func (c *COINUT) wsSubmitOrder(o *WsSubmitOrderParameters) (*order.Detail, error) { if !c.Websocket.CanUseAuthenticatedEndpoints() { return nil, fmt.Errorf("%v not authorised to submit order", c.Name) } @@ -434,8 +637,8 @@ func (c *COINUT) wsSubmitOrder(o *WsSubmitOrderParameters) (*WsStandardOrderResp var orderSubmissionRequest WsSubmitOrderRequest orderSubmissionRequest.Request = "new_order" orderSubmissionRequest.Nonce = getNonce() - orderSubmissionRequest.InstID = c.instrumentMap.LookupID(curr) - orderSubmissionRequest.Qty = o.Amount + orderSubmissionRequest.InstrumentID = c.instrumentMap.LookupID(curr) + orderSubmissionRequest.Quantity = o.Amount orderSubmissionRequest.Price = o.Price orderSubmissionRequest.Side = string(o.Side) @@ -446,107 +649,36 @@ func (c *COINUT) wsSubmitOrder(o *WsSubmitOrderParameters) (*WsStandardOrderResp if err != nil { return nil, err } - var standardOrder WsStandardOrderResponse - standardOrder, err = c.wsStandardiseOrderResponse(resp) + var incoming wsOrderContainer + err = json.Unmarshal(resp, &incoming) if err != nil { return nil, err } - if standardOrder.Status[0] != "OK" { - return &standardOrder, fmt.Errorf("%v order submission failed. %v", c.Name, standardOrder) - } - if len(standardOrder.Reasons) > 0 && standardOrder.Reasons[0] != "" { - return &standardOrder, fmt.Errorf("%v order submission failed. %v", c.Name, standardOrder.Reasons[0]) - } - return &standardOrder, nil -} - -func (c *COINUT) wsStandardiseOrderResponse(resp []byte) (WsStandardOrderResponse, error) { - var response WsStandardOrderResponse - var incoming wsResponse - err := json.Unmarshal(resp, &incoming) + var ord *order.Detail + ord, err = c.parseOrderContainer(&incoming) if err != nil { - return response, err + return nil, err } - switch incoming.Reply { - case "order_accepted": - var orderAccepted WsOrderAcceptedResponse - err := json.Unmarshal(resp, &orderAccepted) - if err != nil { - return response, err - } - response = WsStandardOrderResponse{ - InstID: orderAccepted.InstID, - Nonce: orderAccepted.Nonce, - OpenQty: orderAccepted.OpenQty, - OrderID: orderAccepted.OrderID, - OrderType: orderAccepted.Reply, - Price: orderAccepted.OrderPrice, - Qty: orderAccepted.Qty, - Side: orderAccepted.Side, - Status: orderAccepted.Status, - TransID: orderAccepted.TransID, - ClientOrdID: orderAccepted.ClientOrdID, - } - case "order_filled": - var orderFilled WsOrderFilledResponse - err := json.Unmarshal(resp, &orderFilled) - if err != nil { - return response, err - } - response = WsStandardOrderResponse{ - InstID: orderFilled.Order.InstID, - Nonce: orderFilled.Nonce, - OpenQty: orderFilled.Order.OpenQty, - OrderID: orderFilled.Order.OrderID, - OrderType: orderFilled.Reply, - Price: orderFilled.Order.Price, - Qty: orderFilled.Order.Qty, - Side: orderFilled.Order.Side, - Status: orderFilled.Status, - TransID: orderFilled.TransID, - ClientOrdID: orderFilled.Order.ClientOrdID, - } - case "order_rejected": - var orderRejected WsOrderRejectedResponse - err := json.Unmarshal(resp, &orderRejected) - if err != nil { - return response, err - } - response = WsStandardOrderResponse{ - InstID: orderRejected.InstID, - Nonce: orderRejected.Nonce, - OpenQty: orderRejected.OpenQty, - OrderID: orderRejected.OrderID, - OrderType: orderRejected.Reply, - Price: orderRejected.Price, - Qty: orderRejected.Qty, - Side: orderRejected.Side, - Status: orderRejected.Status, - TransID: orderRejected.TransID, - ClientOrdID: orderRejected.ClientOrdID, - Reasons: orderRejected.Reasons, - } - } - return response, nil + return ord, nil } -func (c *COINUT) wsSubmitOrders(orders []WsSubmitOrderParameters) ([]WsStandardOrderResponse, []error) { - var errors []error - var ordersResponse []WsStandardOrderResponse +func (c *COINUT) wsSubmitOrders(orders []WsSubmitOrderParameters) ([]order.Detail, []error) { + var errs []error + var ordersResponse []order.Detail if !c.Websocket.CanUseAuthenticatedEndpoints() { - errors = append(errors, fmt.Errorf("%v not authorised to submit orders", c.Name)) - return nil, errors + errs = append(errs, fmt.Errorf("%v not authorised to submit orders", c.Name)) + return nil, errs } orderRequest := WsSubmitOrdersRequest{} for i := range orders { curr := c.FormatExchangeCurrency(orders[i].Currency, asset.Spot).String() orderRequest.Orders = append(orderRequest.Orders, WsSubmitOrdersRequestData{ - Qty: orders[i].Amount, - Price: orders[i].Price, - Side: string(orders[i].Side), - InstID: c.instrumentMap.LookupID(curr), - ClientOrdID: i + 1, + Quantity: orders[i].Amount, + Price: orders[i].Price, + Side: string(orders[i].Side), + InstrumentID: c.instrumentMap.LookupID(curr), + ClientOrderID: i + 1, }) } @@ -554,44 +686,25 @@ func (c *COINUT) wsSubmitOrders(orders []WsSubmitOrderParameters) ([]WsStandardO orderRequest.Request = "new_orders" resp, err := c.WebsocketConn.SendMessageReturnResponse(orderRequest.Nonce, orderRequest) if err != nil { - errors = append(errors, err) - return nil, errors + errs = append(errs, err) + return nil, errs } - var incoming []interface{} + var incoming []wsOrderContainer err = json.Unmarshal(resp, &incoming) if err != nil { - errors = append(errors, err) - return nil, errors + errs = append(errs, err) + return nil, errs } for i := range incoming { - var individualJSON []byte - individualJSON, err = json.Marshal(incoming[i]) + o, err := c.parseOrderContainer(&incoming[i]) if err != nil { - errors = append(errors, err) + errs = append(errs, err) continue } - standardOrder, err := c.wsStandardiseOrderResponse(individualJSON) - if err != nil { - errors = append(errors, err) - continue - } - if standardOrder.Status[0] != "OK" { - errors = append(errors, fmt.Errorf("%v order submission failed. %v", c.Name, standardOrder)) - continue - } - if len(standardOrder.Reasons) > 0 && standardOrder.Reasons[0] != "" { - errors = append(errors, fmt.Errorf("%v order submission failed for currency %v and orderID %v, message %v ", - c.Name, - c.instrumentMap.LookupInstrument(standardOrder.InstID), - standardOrder.OrderID, - standardOrder.Reasons[0])) - - continue - } - ordersResponse = append(ordersResponse, standardOrder) + ordersResponse = append(ordersResponse, *o) } - return ordersResponse, errors + return ordersResponse, errs } func (c *COINUT) wsGetOpenOrders(curr string) (*WsUserOpenOrdersResponse, error) { @@ -602,7 +715,7 @@ func (c *COINUT) wsGetOpenOrders(curr string) (*WsUserOpenOrdersResponse, error) var openOrdersRequest WsGetOpenOrdersRequest openOrdersRequest.Request = "user_open_orders" openOrdersRequest.Nonce = getNonce() - openOrdersRequest.InstID = c.instrumentMap.LookupID(curr) + openOrdersRequest.InstrumentID = c.instrumentMap.LookupID(curr) resp, err := c.WebsocketConn.SendMessageReturnResponse(openOrdersRequest.Nonce, openOrdersRequest) if err != nil { @@ -628,7 +741,7 @@ func (c *COINUT) wsCancelOrder(cancellation *WsCancelOrderParameters) (*CancelOr curr := c.FormatExchangeCurrency(cancellation.Currency, asset.Spot).String() var cancellationRequest WsCancelOrderRequest cancellationRequest.Request = "cancel_order" - cancellationRequest.InstID = c.instrumentMap.LookupID(curr) + cancellationRequest.InstrumentID = c.instrumentMap.LookupID(curr) cancellationRequest.OrderID = cancellation.OrderID cancellationRequest.Nonce = getNonce() diff --git a/exchanges/coinut/coinut_wrapper.go b/exchanges/coinut/coinut_wrapper.go index 5fc94839..2fb19ac0 100644 --- a/exchanges/coinut/coinut_wrapper.go +++ b/exchanges/coinut/coinut_wrapper.go @@ -242,7 +242,7 @@ func (c *COINUT) FetchTradablePairs(asset asset.Item) ([]string, error) { instruments = resp.Instruments var pairs []string for i := range instruments { - c.instrumentMap.Seed(instruments[i][0].Base+instruments[i][0].Quote, instruments[i][0].InstID) + c.instrumentMap.Seed(instruments[i][0].Base+instruments[i][0].Quote, instruments[i][0].InstrumentID) p := instruments[i][0].Base + c.GetPairFormat(asset, false).Delimiter + instruments[i][0].Quote pairs = append(pairs, p) } @@ -481,17 +481,17 @@ func (c *COINUT) SubmitOrder(o *order.Submit) (order.SubmitResponse, error) { } if c.Websocket.CanUseAuthenticatedWebsocketForWrapper() { - var response *WsStandardOrderResponse + var response *order.Detail response, err = c.wsSubmitOrder(&WsSubmitOrderParameters{ Currency: o.Pair, - Side: o.OrderSide, + Side: o.Side, Amount: o.Amount, Price: o.Price, }) if err != nil { return submitOrderResponse, err } - submitOrderResponse.OrderID = strconv.FormatInt(response.OrderID, 10) + submitOrderResponse.OrderID = response.ID submitOrderResponse.IsOrderPlaced = true } else { err = c.loadInstrumentsIfNotLoaded() @@ -507,7 +507,7 @@ func (c *COINUT) SubmitOrder(o *order.Submit) (order.SubmitResponse, error) { var APIResponse interface{} var clientIDInt uint64 - isBuyOrder := o.OrderSide == order.Buy + isBuyOrder := o.Side == order.Buy clientIDInt, err = strconv.ParseUint(o.ClientID, 0, 32) if err != nil { return submitOrderResponse, err @@ -550,26 +550,26 @@ func (c *COINUT) CancelOrder(o *order.Cancel) error { if err != nil { return err } - orderIDInt, err := strconv.ParseInt(o.OrderID, 10, 64) + orderIDInt, err := strconv.ParseInt(o.ID, 10, 64) if err != nil { return err } currencyID := c.instrumentMap.LookupID(c.FormatExchangeCurrency( - o.CurrencyPair, + o.Pair, asset.Spot).String(), ) if c.Websocket.CanUseAuthenticatedWebsocketForWrapper() { var resp *CancelOrdersResponse resp, err = c.wsCancelOrder(&WsCancelOrderParameters{ - Currency: o.CurrencyPair, + Currency: o.Pair, OrderID: orderIDInt, }) if err != nil { return err } if len(resp.Status) >= 1 && resp.Status[0] != "OK" { - return errors.New(c.Name + " - Failed to cancel order " + o.OrderID) + return errors.New(c.Name + " - Failed to cancel order " + o.ID) } } else { if currencyID == 0 { @@ -593,15 +593,15 @@ func (c *COINUT) CancelAllOrders(details *order.Cancel) (order.CancelAllResponse } cancelAllOrdersResponse.Status = make(map[string]string) if c.Websocket.CanUseAuthenticatedWebsocketForWrapper() { - openOrders, err := c.wsGetOpenOrders(details.CurrencyPair.String()) + openOrders, err := c.wsGetOpenOrders(details.Pair.String()) if err != nil { return cancelAllOrdersResponse, err } var ordersToCancel []WsCancelOrderParameters for i := range openOrders.Orders { - if openOrders.Orders[i].InstID == c.instrumentMap.LookupID(c.FormatExchangeCurrency(details.CurrencyPair, asset.Spot).String()) { + if openOrders.Orders[i].InstrumentID == c.instrumentMap.LookupID(c.FormatExchangeCurrency(details.Pair, asset.Spot).String()) { ordersToCancel = append(ordersToCancel, WsCancelOrderParameters{ - Currency: details.CurrencyPair, + Currency: details.Pair, OrderID: openOrders.Orders[i].OrderID, }) } @@ -619,7 +619,7 @@ func (c *COINUT) CancelAllOrders(details *order.Cancel) (order.CancelAllResponse var allTheOrders []OrderResponse ids := c.instrumentMap.GetInstrumentIDs() for x := range ids { - if ids[x] == c.instrumentMap.LookupID(c.FormatExchangeCurrency(details.CurrencyPair, asset.Spot).String()) { + if ids[x] == c.instrumentMap.LookupID(c.FormatExchangeCurrency(details.Pair, asset.Spot).String()) { openOrders, err := c.GetOpenOrders(ids[x]) if err != nil { return cancelAllOrdersResponse, err @@ -704,9 +704,9 @@ func (c *COINUT) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, e } var orders []order.Detail var currenciesToCheck []string - if len(req.Currencies) == 0 { - for i := range req.Currencies { - currenciesToCheck = append(currenciesToCheck, c.FormatExchangeCurrency(req.Currencies[i], asset.Spot).String()) + if len(req.Pairs) == 0 { + for i := range req.Pairs { + currenciesToCheck = append(currenciesToCheck, c.FormatExchangeCurrency(req.Pairs[i], asset.Spot).String()) } } else { for k := range c.instrumentMap.Instruments { @@ -723,32 +723,27 @@ func (c *COINUT) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, e orders = append(orders, order.Detail{ Exchange: c.Name, ID: strconv.FormatInt(openOrders.Orders[i].OrderID, 10), - CurrencyPair: c.FormatExchangeCurrency(currency.NewPairFromString(currenciesToCheck[x]), asset.Spot), - OrderSide: order.Side(openOrders.Orders[i].Side), - OrderDate: time.Unix(0, openOrders.Orders[i].Timestamp), + Pair: c.FormatExchangeCurrency(currency.NewPairFromString(currenciesToCheck[x]), asset.Spot), + Side: order.Side(openOrders.Orders[i].Side), + Date: time.Unix(0, openOrders.Orders[i].Timestamp), Status: order.Active, Price: openOrders.Orders[i].Price, - Amount: openOrders.Orders[i].Qty, - ExecutedAmount: openOrders.Orders[i].Qty - openOrders.Orders[i].OpenQty, - RemainingAmount: openOrders.Orders[i].OpenQty, + Amount: openOrders.Orders[i].Quantity, + ExecutedAmount: openOrders.Orders[i].Quantity - openOrders.Orders[i].OpenQuantity, + RemainingAmount: openOrders.Orders[i].OpenQuantity, }) } } } else { var instrumentsToUse []int64 - if len(req.Currencies) > 0 { - for x := range req.Currencies { - curr := c.FormatExchangeCurrency(req.Currencies[x], - asset.Spot).String() - instrumentsToUse = append(instrumentsToUse, - c.instrumentMap.LookupID(curr)) - } - } else { - instrumentsToUse = c.instrumentMap.GetInstrumentIDs() + for x := range req.Pairs { + curr := c.FormatExchangeCurrency(req.Pairs[x], + asset.Spot).String() + instrumentsToUse = append(instrumentsToUse, + c.instrumentMap.LookupID(curr)) } - if len(instrumentsToUse) == 0 { - return nil, errors.New("no instrument IDs to use") + instrumentsToUse = c.instrumentMap.GetInstrumentIDs() } for x := range instrumentsToUse { @@ -764,20 +759,20 @@ func (c *COINUT) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, e orderSide := order.Side(strings.ToUpper(openOrders.Orders[y].Side)) orderDate := time.Unix(openOrders.Orders[y].Timestamp, 0) orders = append(orders, order.Detail{ - ID: strconv.FormatInt(openOrders.Orders[y].OrderID, 10), - Amount: openOrders.Orders[y].Quantity, - Price: openOrders.Orders[y].Price, - Exchange: c.Name, - OrderSide: orderSide, - OrderDate: orderDate, - CurrencyPair: p, + ID: strconv.FormatInt(openOrders.Orders[y].OrderID, 10), + Amount: openOrders.Orders[y].Quantity, + Price: openOrders.Orders[y].Price, + Exchange: c.Name, + Side: orderSide, + Date: orderDate, + Pair: p, }) } } } order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) - order.FilterOrdersBySide(&orders, req.OrderSide) + order.FilterOrdersBySide(&orders, req.Side) return orders, nil } @@ -790,25 +785,25 @@ func (c *COINUT) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail, e } var allOrders []order.Detail if c.Websocket.CanUseAuthenticatedWebsocketForWrapper() { - for i := range req.Currencies { + for i := range req.Pairs { for j := int64(0); ; j += 100 { - trades, err := c.wsGetTradeHistory(req.Currencies[i], j, 100) + trades, err := c.wsGetTradeHistory(req.Pairs[i], j, 100) if err != nil { return allOrders, err } for x := range trades.Trades { - curr := c.instrumentMap.LookupInstrument(trades.Trades[x].InstID) + curr := c.instrumentMap.LookupInstrument(trades.Trades[x].InstrumentID) allOrders = append(allOrders, order.Detail{ Exchange: c.Name, ID: strconv.FormatInt(trades.Trades[x].OrderID, 10), - CurrencyPair: currency.NewPairFromString(curr), - OrderSide: order.Side(trades.Trades[x].Side), - OrderDate: time.Unix(0, trades.Trades[x].Timestamp), + Pair: currency.NewPairFromString(curr), + Side: order.Side(trades.Trades[x].Side), + Date: time.Unix(0, trades.Trades[x].Timestamp), Status: order.Filled, Price: trades.Trades[x].Price, - Amount: trades.Trades[x].Qty, - ExecutedAmount: trades.Trades[x].Qty, - RemainingAmount: trades.Trades[x].OpenQty, + Amount: trades.Trades[x].Quantity, + ExecutedAmount: trades.Trades[x].Quantity, + RemainingAmount: trades.Trades[x].OpenQuantity, }) } if len(trades.Trades) < 100 { @@ -818,21 +813,16 @@ func (c *COINUT) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail, e } } else { var instrumentsToUse []int64 - if len(req.Currencies) > 0 { - for x := range req.Currencies { - curr := c.FormatExchangeCurrency(req.Currencies[x], - asset.Spot).String() - instrumentID := c.instrumentMap.LookupID(curr) - if instrumentID > 0 { - instrumentsToUse = append(instrumentsToUse, instrumentID) - } + for x := range req.Pairs { + curr := c.FormatExchangeCurrency(req.Pairs[x], + asset.Spot).String() + instrumentID := c.instrumentMap.LookupID(curr) + if instrumentID > 0 { + instrumentsToUse = append(instrumentsToUse, instrumentID) } - } else { - instrumentsToUse = c.instrumentMap.GetInstrumentIDs() } - if len(instrumentsToUse) == 0 { - return nil, errors.New("no instrument IDs to use") + instrumentsToUse = c.instrumentMap.GetInstrumentIDs() } for x := range instrumentsToUse { orders, err := c.GetTradeHistory(instrumentsToUse[x], -1, -1) @@ -847,20 +837,20 @@ func (c *COINUT) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail, e orderSide := order.Side(strings.ToUpper(orders.Trades[y].Order.Side)) orderDate := time.Unix(orders.Trades[y].Order.Timestamp, 0) allOrders = append(allOrders, order.Detail{ - ID: strconv.FormatInt(orders.Trades[y].Order.OrderID, 10), - Amount: orders.Trades[y].Order.Quantity, - Price: orders.Trades[y].Order.Price, - Exchange: c.Name, - OrderSide: orderSide, - OrderDate: orderDate, - CurrencyPair: p, + ID: strconv.FormatInt(orders.Trades[y].Order.OrderID, 10), + Amount: orders.Trades[y].Order.Quantity, + Price: orders.Trades[y].Order.Price, + Exchange: c.Name, + Side: orderSide, + Date: orderDate, + Pair: p, }) } } } order.FilterOrdersByTickRange(&allOrders, req.StartTicks, req.EndTicks) - order.FilterOrdersBySide(&allOrders, req.OrderSide) + order.FilterOrdersBySide(&allOrders, req.Side) return allOrders, nil } diff --git a/exchanges/exchange.go b/exchanges/exchange.go index 40fe57ac..b4026650 100644 --- a/exchanges/exchange.go +++ b/exchanges/exchange.go @@ -26,7 +26,7 @@ const ( // DefaultHTTPTimeout is the default HTTP/HTTPS Timeout for exchange requests DefaultHTTPTimeout = time.Second * 15 // DefaultWebsocketResponseCheckTimeout is the default delay in checking for an expected websocket response - DefaultWebsocketResponseCheckTimeout = time.Millisecond * 30 + DefaultWebsocketResponseCheckTimeout = time.Millisecond * 50 // DefaultWebsocketResponseMaxLimit is the default max wait for an expected websocket response before a timeout DefaultWebsocketResponseMaxLimit = time.Second * 7 // DefaultWebsocketOrderbookBufferLimit is the maximum number of orderbook updates that get stored before being applied @@ -213,9 +213,10 @@ func (e *Base) GetAssetTypes() asset.Items { // 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 + assetTypes := e.GetAssetTypes() + for i := range assetTypes { + if e.GetEnabledPairs(assetTypes[i]).Contains(c, true) { + return assetTypes[i], nil } } return "", errors.New("asset type not associated with currency pair") @@ -340,6 +341,24 @@ func (e *Base) GetEnabledPairs(assetType asset.Item) currency.Pairs { return pairs.Format(format.Delimiter, format.Index, format.Uppercase) } +// GetRequestFormattedPairAndAssetType is a method that returns the enabled currency pair of +// along with its asset type. Only use when there is no chance of the same name crossing over +func (e *Base) GetRequestFormattedPairAndAssetType(p string) (currency.Pair, asset.Item, error) { + assetTypes := e.GetAssetTypes() + var response currency.Pair + for i := range assetTypes { + format := e.GetPairFormat(assetTypes[i], true) + pairs := e.CurrencyPairs.GetPairs(assetTypes[i], true) + for j := range pairs { + formattedPair := pairs[j].Format(format.Delimiter, format.Uppercase) + if strings.EqualFold(formattedPair.String(), p) { + return formattedPair, assetTypes[i], nil + } + } + } + return response, "", errors.New("pair not found: " + p) +} + // GetAvailablePairs is a method that returns the available currency pairs // of the exchange by asset type func (e *Base) GetAvailablePairs(assetType asset.Item) currency.Pairs { diff --git a/exchanges/exchange_test.go b/exchanges/exchange_test.go index e01c6754..42c33dab 100644 --- a/exchanges/exchange_test.go +++ b/exchanges/exchange_test.go @@ -1445,3 +1445,39 @@ func TestGetAssetType(t *testing.T) { t.Error("should be spot but is", a) } } + +func TestGetFormattedPairAndAssetType(t *testing.T) { + t.Parallel() + b := Base{ + Config: &config.ExchangeConfig{}, + } + b.SetCurrencyPairFormat() + b.Config.CurrencyPairs.UseGlobalFormat = true + b.CurrencyPairs.UseGlobalFormat = true + pFmt := ¤cy.PairFormat{ + Delimiter: "#", + } + b.CurrencyPairs.RequestFormat = pFmt + b.CurrencyPairs.ConfigFormat = pFmt + b.CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore) + b.CurrencyPairs.Pairs[asset.Spot] = ¤cy.PairStore{ + Enabled: currency.Pairs{ + currency.NewPair(currency.BTC, currency.USD), + }, + } + b.CurrencyPairs.AssetTypes = asset.Items{asset.Spot} + p, a, err := b.GetRequestFormattedPairAndAssetType("btc#usd") + if err != nil { + t.Error(err) + } + if p.String() != "btc#usd" { + t.Error("Expected pair to match") + } + if a != asset.Spot { + t.Error("Expected spot asset") + } + _, _, err = b.GetRequestFormattedPairAndAssetType("btcusd") + if err == nil { + t.Error("Expected error") + } +} diff --git a/exchanges/exchange_types.go b/exchanges/exchange_types.go index f5a1ee04..27f955bf 100644 --- a/exchanges/exchange_types.go +++ b/exchanges/exchange_types.go @@ -110,7 +110,7 @@ type FeeBuilder struct { // TradeHistory holds exchange history data type TradeHistory struct { Timestamp time.Time - TID int64 + TID string Price float64 Amount float64 Exchange string diff --git a/exchanges/exmo/exmo_test.go b/exchanges/exmo/exmo_test.go index 111b892b..47677ec4 100644 --- a/exchanges/exmo/exmo_test.go +++ b/exchanges/exmo/exmo_test.go @@ -262,7 +262,7 @@ func TestFormatWithdrawPermissions(t *testing.T) { func TestGetActiveOrders(t *testing.T) { var getOrdersRequest = order.GetOrdersRequest{ - OrderType: order.AnyType, + Type: order.AnyType, } _, err := e.GetActiveOrders(&getOrdersRequest) @@ -275,11 +275,11 @@ func TestGetActiveOrders(t *testing.T) { func TestGetOrderHistory(t *testing.T) { var getOrdersRequest = order.GetOrdersRequest{ - OrderType: order.AnyType, + Type: order.AnyType, } currPair := currency.NewPair(currency.BTC, currency.USD) currPair.Delimiter = "_" - getOrdersRequest.Currencies = []currency.Pair{currPair} + getOrdersRequest.Pairs = []currency.Pair{currPair} _, err := e.GetOrderHistory(&getOrdersRequest) if areTestAPIKeysSet() && err != nil { @@ -306,11 +306,11 @@ func TestSubmitOrder(t *testing.T) { Base: currency.BTC, Quote: currency.USD, }, - OrderSide: order.Buy, - OrderType: order.Limit, - Price: 1, - Amount: 1, - ClientID: "meowOrder", + Side: order.Buy, + Type: order.Limit, + Price: 1, + Amount: 1, + ClientID: "meowOrder", } response, err := e.SubmitOrder(orderSubmission) if areTestAPIKeysSet() && (err != nil || !response.IsOrderPlaced) { @@ -327,10 +327,10 @@ func TestCancelExchangeOrder(t *testing.T) { currencyPair := currency.NewPair(currency.LTC, currency.BTC) var orderCancellation = &order.Cancel{ - OrderID: "1", + ID: "1", WalletAddress: core.BitcoinDonationAddress, AccountID: "1", - CurrencyPair: currencyPair, + Pair: currencyPair, } err := e.CancelOrder(orderCancellation) @@ -349,10 +349,10 @@ func TestCancelAllExchangeOrders(t *testing.T) { currencyPair := currency.NewPair(currency.LTC, currency.BTC) var orderCancellation = &order.Cancel{ - OrderID: "1", + ID: "1", WalletAddress: core.BitcoinDonationAddress, AccountID: "1", - CurrencyPair: currencyPair, + Pair: currencyPair, } resp, err := e.CancelAllOrders(orderCancellation) diff --git a/exchanges/exmo/exmo_wrapper.go b/exchanges/exmo/exmo_wrapper.go index e98c355a..ed137e7e 100644 --- a/exchanges/exmo/exmo_wrapper.go +++ b/exchanges/exmo/exmo_wrapper.go @@ -345,11 +345,11 @@ func (e *EXMO) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) { } var oT string - switch s.OrderType { + switch s.Type { case order.Limit: return submitOrderResponse, errors.New("unsupported order type") case order.Market: - if s.OrderSide == order.Sell { + if s.Side == order.Sell { oT = "market_sell" } else { oT = "market_buy" @@ -368,7 +368,7 @@ func (e *EXMO) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) { } submitOrderResponse.IsOrderPlaced = true - if s.OrderType == order.Market { + if s.Type == order.Market { submitOrderResponse.FullyMatched = true } return submitOrderResponse, nil @@ -382,7 +382,7 @@ func (e *EXMO) ModifyOrder(action *order.Modify) (string, error) { // CancelOrder cancels an order by its corresponding ID number func (e *EXMO) CancelOrder(order *order.Cancel) error { - orderIDInt, err := strconv.ParseInt(order.OrderID, 10, 64) + orderIDInt, err := strconv.ParseInt(order.ID, 10, 64) if err != nil { return err } @@ -484,31 +484,31 @@ func (e *EXMO) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, err orderDate := time.Unix(resp[i].Created, 0) orderSide := order.Side(strings.ToUpper(resp[i].Type)) orders = append(orders, order.Detail{ - ID: strconv.FormatInt(resp[i].OrderID, 10), - Amount: resp[i].Quantity, - OrderDate: orderDate, - Price: resp[i].Price, - OrderSide: orderSide, - Exchange: e.Name, - CurrencyPair: symbol, + ID: strconv.FormatInt(resp[i].OrderID, 10), + Amount: resp[i].Quantity, + Date: orderDate, + Price: resp[i].Price, + Side: orderSide, + Exchange: e.Name, + Pair: symbol, }) } order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) - order.FilterOrdersBySide(&orders, req.OrderSide) + order.FilterOrdersBySide(&orders, req.Side) return orders, nil } // GetOrderHistory retrieves account order information // Can Limit response to specific order status func (e *EXMO) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail, error) { - if len(req.Currencies) == 0 { + if len(req.Pairs) == 0 { return nil, errors.New("currency must be supplied") } var allTrades []UserTrades - for i := range req.Currencies { - resp, err := e.GetUserTrades(e.FormatExchangeCurrency(req.Currencies[i], asset.Spot).String(), "", "10000") + for i := range req.Pairs { + resp, err := e.GetUserTrades(e.FormatExchangeCurrency(req.Pairs[i], asset.Spot).String(), "", "10000") if err != nil { return nil, err } @@ -523,18 +523,18 @@ func (e *EXMO) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail, err orderDate := time.Unix(allTrades[i].Date, 0) orderSide := order.Side(strings.ToUpper(allTrades[i].Type)) orders = append(orders, order.Detail{ - ID: strconv.FormatInt(allTrades[i].TradeID, 10), - Amount: allTrades[i].Quantity, - OrderDate: orderDate, - Price: allTrades[i].Price, - OrderSide: orderSide, - Exchange: e.Name, - CurrencyPair: symbol, + ID: strconv.FormatInt(allTrades[i].TradeID, 10), + Amount: allTrades[i].Quantity, + Date: orderDate, + Price: allTrades[i].Price, + Side: orderSide, + Exchange: e.Name, + Pair: symbol, }) } order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) - order.FilterOrdersBySide(&orders, req.OrderSide) + order.FilterOrdersBySide(&orders, req.Side) return orders, nil } diff --git a/exchanges/gateio/gateio_test.go b/exchanges/gateio/gateio_test.go index c57682ff..e48870b1 100644 --- a/exchanges/gateio/gateio_test.go +++ b/exchanges/gateio/gateio_test.go @@ -274,7 +274,7 @@ func TestFormatWithdrawPermissions(t *testing.T) { func TestGetActiveOrders(t *testing.T) { var getOrdersRequest = order.GetOrdersRequest{ - OrderType: order.AnyType, + Type: order.AnyType, } _, err := g.GetActiveOrders(&getOrdersRequest) @@ -287,12 +287,12 @@ func TestGetActiveOrders(t *testing.T) { func TestGetOrderHistory(t *testing.T) { var getOrdersRequest = order.GetOrdersRequest{ - OrderType: order.AnyType, + Type: order.AnyType, } currPair := currency.NewPair(currency.LTC, currency.BTC) currPair.Delimiter = "_" - getOrdersRequest.Currencies = []currency.Pair{currPair} + getOrdersRequest.Pairs = []currency.Pair{currPair} _, err := g.GetOrderHistory(&getOrdersRequest) if areTestAPIKeysSet() && err != nil { @@ -319,11 +319,11 @@ func TestSubmitOrder(t *testing.T) { Base: currency.LTC, Quote: currency.BTC, }, - OrderSide: order.Buy, - OrderType: order.Limit, - Price: 1, - Amount: 1, - ClientID: "meowOrder", + Side: order.Buy, + Type: order.Limit, + Price: 1, + Amount: 1, + ClientID: "meowOrder", } response, err := g.SubmitOrder(orderSubmission) if areTestAPIKeysSet() && (err != nil || !response.IsOrderPlaced) { @@ -340,10 +340,10 @@ func TestCancelExchangeOrder(t *testing.T) { currencyPair := currency.NewPair(currency.LTC, currency.BTC) var orderCancellation = &order.Cancel{ - OrderID: "1", + ID: "1", WalletAddress: core.BitcoinDonationAddress, AccountID: "1", - CurrencyPair: currencyPair, + Pair: currencyPair, } err := g.CancelOrder(orderCancellation) @@ -362,10 +362,10 @@ func TestCancelAllExchangeOrders(t *testing.T) { currencyPair := currency.NewPair(currency.LTC, currency.BTC) var orderCancellation = &order.Cancel{ - OrderID: "1", + ID: "1", WalletAddress: core.BitcoinDonationAddress, AccountID: "1", - CurrencyPair: currencyPair, + Pair: currencyPair, } resp, err := g.CancelAllOrders(orderCancellation) @@ -497,9 +497,7 @@ func TestWsGetBalance(t *testing.T) { if err != nil { t.Fatal(err) } - go g.WsHandleData() - g.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride() - g.Websocket.TrafficAlert = sharedtestvalues.GetWebsocketStructChannelOverride() + go g.wsReadData() resp, err := g.wsServerSignIn() if err != nil { t.Fatal(err) @@ -535,9 +533,7 @@ func TestWsGetOrderInfo(t *testing.T) { if err != nil { t.Fatal(err) } - go g.WsHandleData() - g.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride() - g.Websocket.TrafficAlert = sharedtestvalues.GetWebsocketStructChannelOverride() + go g.wsReadData() resp, err := g.wsServerSignIn() if err != nil { t.Fatal(err) @@ -568,15 +564,29 @@ func setupWSTestAuth(t *testing.T) { } var dialer websocket.Dialer err := g.WebsocketConn.Dial(&dialer, http.Header{}) + + g.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride() + g.Websocket.TrafficAlert = sharedtestvalues.GetWebsocketStructChannelOverride() if err != nil { t.Fatal(err) } - go g.WsHandleData() - g.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride() - g.Websocket.TrafficAlert = sharedtestvalues.GetWebsocketStructChannelOverride() + go g.wsReadData() wsSetupRan = true } +// TestWsUnsubscribe dials websocket, sends an unsubscribe request. +func TestWsUnsubscribe(t *testing.T) { + setupWSTestAuth(t) + g.Verbose = true + err := g.Unsubscribe(wshandler.WebsocketChannelSubscription{ + Channel: "ticker.subscribe", + Currency: currency.NewPairWithDelimiter(currency.BTC.String(), currency.USDT.String(), "_"), + }) + if err != nil { + t.Error(err) + } +} + // TestWsSubscribe dials websocket, sends a subscribe request. func TestWsSubscribe(t *testing.T) { setupWSTestAuth(t) @@ -589,13 +599,146 @@ func TestWsSubscribe(t *testing.T) { } } -// TestWsUnsubscribe dials websocket, sends an unsubscribe request. -func TestWsUnsubscribe(t *testing.T) { - setupWSTestAuth(t) - err := g.Unsubscribe(wshandler.WebsocketChannelSubscription{ - Channel: "ticker.subscribe", - Currency: currency.NewPairWithDelimiter(currency.BTC.String(), currency.USDT.String(), "_"), - }) +func TestWsTicker(t *testing.T) { + pressXToJSON := []byte(`{ + "method": "ticker.update", + "params": + [ + "BTC_USDT", + { + "period": 86400, + "open": "0", + "close": "0", + "high": "0", + "low": "0", + "last": "0.2844", + "change": "0", + "quoteVolume": "0", + "baseVolume": "0" + } + ], + "id": null +}`) + err := g.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsTrade(t *testing.T) { + pressXToJSON := []byte(`{ + "method": "trades.update", + "params": + [ + "BTC_USDT", + [ + { + "id": 7172173, + "time": 1523339279.761838, + "price": "398.59", + "amount": "0.027", + "type": "buy" + } + ] + ], + "id": null + } +`) + err := g.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsDepth(t *testing.T) { + pressXToJSON := []byte(`{ + "method": "depth.update", + "params": [ + true, + { + "asks": [ + [ + "8000.00", + "9.6250" + ] + ], + "bids": [ + [ + "8000.00", + "9.6250" + ] + ] + }, + "BTC_USDT" + ], + "id": null + }`) + err := g.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsKLine(t *testing.T) { + pressXToJSON := []byte(`{ + "method": "kline.update", + "params": + [ + [ + 1492358400, + "7000.00", + "8000.0", + "8100.00", + "6800.00", + "1000.00", + "123456.00", + "BTC_USDT" + ] + ], + "id": null +}`) + err := g.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsOrderUpdate(t *testing.T) { + pressXToJSON := []byte(`{ + "method": "order.update", + "params": [ + 3, + { + "id": 34628963, + "market": "BTC_USDT", + "orderType": 1, + "type": 2, + "user": 602123, + "ctime": 1523013969.6271579, + "mtime": 1523013969.6271579, + "price": "0.1", + "amount": "1000", + "left": "1000", + "filledAmount": "0", + "filledTotal": "0", + "dealFee": "0" + } + ], + "id": null +}`) + err := g.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsBalanceUpdate(t *testing.T) { + pressXToJSON := []byte(`{ + "method": "balance.update", + "params": [{"EOS": {"available": "96.765323611874", "freeze": "11"}}], + "id": 1234 +}`) + err := g.wsHandleData(pressXToJSON) if err != nil { t.Error(err) } diff --git a/exchanges/gateio/gateio_types.go b/exchanges/gateio/gateio_types.go index 283cfbf4..e23cca47 100644 --- a/exchanges/gateio/gateio_types.go +++ b/exchanges/gateio/gateio_types.go @@ -488,3 +488,15 @@ type WsGetBalanceResponseData struct { Available float64 `json:"available,string"` Freeze float64 `json:"freeze,string"` } + +type wsBalanceSubscription struct { + Method string `json:"method"` + Parameters []map[string]WsGetBalanceResponseData `json:"params"` + ID int64 `json:"id"` +} + +type wsOrderUpdate struct { + ID int64 `json:"id"` + Method string `json:"method"` + Params []interface{} `json:"params"` +} diff --git a/exchanges/gateio/gateio_websocket.go b/exchanges/gateio/gateio_websocket.go index 2547d791..1bb58f34 100644 --- a/exchanges/gateio/gateio_websocket.go +++ b/exchanges/gateio/gateio_websocket.go @@ -10,10 +10,12 @@ import ( "time" "github.com/gorilla/websocket" + "github.com/thrasher-corp/gocryptotrader/common/convert" "github.com/thrasher-corp/gocryptotrader/common/crypto" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" @@ -36,7 +38,7 @@ func (g *Gateio) WsConnect() error { if err != nil { return err } - go g.WsHandleData() + go g.wsReadData() _, err = g.wsServerSignIn() if err != nil { log.Errorf(log.ExchangeSys, "%v - authentication failed: %v\n", g.Name, err) @@ -76,9 +78,8 @@ func (g *Gateio) wsServerSignIn() (*WebsocketAuthenticationResponse, error) { return &response, nil } -// WsHandleData handles all the websocket data coming from the websocket -// connection -func (g *Gateio) WsHandleData() { +// wsReadData receives and passes on websocket messages for processing +func (g *Gateio) wsReadData() { g.Websocket.Wg.Add(1) defer func() { @@ -97,203 +98,326 @@ func (g *Gateio) WsHandleData() { return } g.Websocket.TrafficAlert <- struct{}{} - var result WebsocketResponse - err = json.Unmarshal(resp.Raw, &result) + err = g.wsHandleData(resp.Raw) if err != nil { g.Websocket.DataHandler <- err - continue - } - - if result.ID > 0 { - g.WebsocketConn.AddResponseWithID(result.ID, resp.Raw) - continue - } - - if result.Error.Code != 0 { - if strings.Contains(result.Error.Message, "authentication") { - g.Websocket.DataHandler <- fmt.Errorf("%v - authentication failed: %v", g.Name, err) - g.Websocket.SetCanUseAuthenticatedEndpoints(false) - continue - } - g.Websocket.DataHandler <- fmt.Errorf("%v error %s", - g.Name, result.Error.Message) - continue - } - - switch { - case strings.Contains(result.Method, "ticker"): - var wsTicker WebsocketTicker - var c string - err = json.Unmarshal(result.Params[1], &wsTicker) - if err != nil { - g.Websocket.DataHandler <- err - continue - } - - err = json.Unmarshal(result.Params[0], &c) - if err != nil { - g.Websocket.DataHandler <- err - continue - } - - g.Websocket.DataHandler <- &ticker.Price{ - ExchangeName: g.Name, - Open: wsTicker.Open, - Close: wsTicker.Close, - Volume: wsTicker.BaseVolume, - QuoteVolume: wsTicker.QuoteVolume, - High: wsTicker.High, - Low: wsTicker.Low, - Last: wsTicker.Last, - AssetType: asset.Spot, - Pair: currency.NewPairFromString(c), - } - - case strings.Contains(result.Method, "trades"): - var trades []WebsocketTrade - var c string - err = json.Unmarshal(result.Params[1], &trades) - if err != nil { - g.Websocket.DataHandler <- err - continue - } - - err = json.Unmarshal(result.Params[0], &c) - if err != nil { - g.Websocket.DataHandler <- err - continue - } - - for i := range trades { - g.Websocket.DataHandler <- wshandler.TradeData{ - Timestamp: time.Now(), - CurrencyPair: currency.NewPairFromString(c), - AssetType: asset.Spot, - Exchange: g.Name, - Price: trades[i].Price, - Amount: trades[i].Amount, - Side: trades[i].Type, - } - } - - case strings.Contains(result.Method, "depth"): - var IsSnapshot bool - var c string - var data = make(map[string][][]string) - err = json.Unmarshal(result.Params[0], &IsSnapshot) - if err != nil { - g.Websocket.DataHandler <- err - continue - } - - err = json.Unmarshal(result.Params[2], &c) - if err != nil { - g.Websocket.DataHandler <- err - continue - } - - err = json.Unmarshal(result.Params[1], &data) - if err != nil { - g.Websocket.DataHandler <- err - continue - } - - var asks, bids []orderbook.Item - - askData, askOk := data["asks"] - for i := range askData { - amount, _ := strconv.ParseFloat(askData[i][1], 64) - price, _ := strconv.ParseFloat(askData[i][0], 64) - asks = append(asks, orderbook.Item{ - Amount: amount, - Price: price, - }) - } - - bidData, bidOk := data["bids"] - for i := range bidData { - amount, _ := strconv.ParseFloat(bidData[i][1], 64) - price, _ := strconv.ParseFloat(bidData[i][0], 64) - bids = append(bids, orderbook.Item{ - Amount: amount, - Price: price, - }) - } - - if !askOk && !bidOk { - g.Websocket.DataHandler <- errors.New("gatio websocket error - cannot access ask or bid data") - } - - if IsSnapshot { - if !askOk { - g.Websocket.DataHandler <- errors.New("gatio websocket error - cannot access ask data") - } - - if !bidOk { - g.Websocket.DataHandler <- errors.New("gatio websocket error - cannot access bid data") - } - - var newOrderBook orderbook.Base - newOrderBook.Asks = asks - newOrderBook.Bids = bids - newOrderBook.AssetType = asset.Spot - newOrderBook.Pair = currency.NewPairFromString(c) - newOrderBook.ExchangeName = g.Name - - err = g.Websocket.Orderbook.LoadSnapshot(&newOrderBook) - if err != nil { - g.Websocket.DataHandler <- err - } - } else { - err = g.Websocket.Orderbook.Update( - &wsorderbook.WebsocketOrderbookUpdate{ - Asks: asks, - Bids: bids, - Pair: currency.NewPairFromString(c), - UpdateTime: time.Now(), - Asset: asset.Spot, - }) - if err != nil { - g.Websocket.DataHandler <- err - } - } - - g.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{ - Pair: currency.NewPairFromString(c), - Asset: asset.Spot, - Exchange: g.Name, - } - - case strings.Contains(result.Method, "kline"): - var data []interface{} - err = json.Unmarshal(result.Params[0], &data) - if err != nil { - g.Websocket.DataHandler <- err - continue - } - - open, _ := strconv.ParseFloat(data[1].(string), 64) - closePrice, _ := strconv.ParseFloat(data[2].(string), 64) - high, _ := strconv.ParseFloat(data[3].(string), 64) - low, _ := strconv.ParseFloat(data[4].(string), 64) - volume, _ := strconv.ParseFloat(data[5].(string), 64) - - g.Websocket.DataHandler <- wshandler.KlineData{ - Timestamp: time.Now(), - Pair: currency.NewPairFromString(data[7].(string)), - AssetType: asset.Spot, - Exchange: g.Name, - OpenPrice: open, - ClosePrice: closePrice, - HighPrice: high, - LowPrice: low, - Volume: volume, - } } } } } +func (g *Gateio) wsHandleData(respRaw []byte) error { + var result WebsocketResponse + err := json.Unmarshal(respRaw, &result) + if err != nil { + return err + } + + if result.ID > 0 { + if g.WebsocketConn.IsIDWaitingForResponse(result.ID) { + g.WebsocketConn.SetResponseIDAndData(result.ID, respRaw) + return nil + } + } + + if result.Error.Code != 0 { + if strings.Contains(result.Error.Message, "authentication") { + g.Websocket.SetCanUseAuthenticatedEndpoints(false) + return fmt.Errorf("%v - authentication failed: %v", g.Name, err) + } + return fmt.Errorf("%v error %s", + g.Name, result.Error.Message) + } + + switch { + case strings.Contains(result.Method, "ticker"): + var wsTicker WebsocketTicker + var c string + err = json.Unmarshal(result.Params[1], &wsTicker) + if err != nil { + return err + } + err = json.Unmarshal(result.Params[0], &c) + if err != nil { + return err + } + + g.Websocket.DataHandler <- &ticker.Price{ + ExchangeName: g.Name, + Open: wsTicker.Open, + Close: wsTicker.Close, + Volume: wsTicker.BaseVolume, + QuoteVolume: wsTicker.QuoteVolume, + High: wsTicker.High, + Low: wsTicker.Low, + Last: wsTicker.Last, + AssetType: asset.Spot, + Pair: currency.NewPairFromString(c), + } + + case strings.Contains(result.Method, "trades"): + var trades []WebsocketTrade + var c string + err = json.Unmarshal(result.Params[1], &trades) + if err != nil { + return err + } + err = json.Unmarshal(result.Params[0], &c) + if err != nil { + return err + } + + for i := range trades { + var tSide order.Side + tSide, err = order.StringToOrderSide(trades[i].Type) + if err != nil { + g.Websocket.DataHandler <- order.ClassificationError{ + Exchange: g.Name, + Err: err, + } + } + g.Websocket.DataHandler <- wshandler.TradeData{ + Timestamp: time.Now(), + CurrencyPair: currency.NewPairFromString(c), + AssetType: asset.Spot, + Exchange: g.Name, + Price: trades[i].Price, + Amount: trades[i].Amount, + Side: tSide, + } + } + case strings.Contains(result.Method, "balance.update"): + var balance wsBalanceSubscription + err = json.Unmarshal(respRaw, &balance) + if err != nil { + return err + } + g.Websocket.DataHandler <- balance + case strings.Contains(result.Method, "order.update"): + var orderUpdate wsOrderUpdate + err = json.Unmarshal(respRaw, &orderUpdate) + if err != nil { + return err + } + invalidJSON := orderUpdate.Params[1].(map[string]interface{}) + oStatus := order.UnknownStatus + oType := order.UnknownType + oSide := order.UnknownSide + switch orderUpdate.Params[0].(float64) { + case 1: + oStatus = order.New + case 2: + oStatus = order.PartiallyFilled + case 3: + oStatus = order.Filled + } + switch invalidJSON["orderType"].(float64) { + case 1: + oType = order.Limit + case 2: + oType = order.Market + } + switch invalidJSON["type"].(float64) { + case 1: + oSide = order.Sell + case 2: + oSide = order.Buy + } + var cTime, cTimeDec, mTime, mTimeDec int64 + var price, amount, filledTotal, left, fee float64 + cTime, cTimeDec, err = convert.SplitFloatDecimals(invalidJSON["ctime"].(float64)) + if err != nil { + return err + } + mTime, mTimeDec, err = convert.SplitFloatDecimals(invalidJSON["mtime"].(float64)) + if err != nil { + return err + } + price, err = strconv.ParseFloat(invalidJSON["price"].(string), 64) + if err != nil { + return err + } + amount, err = strconv.ParseFloat(invalidJSON["amount"].(string), 64) + if err != nil { + return err + } + filledTotal, err = strconv.ParseFloat(invalidJSON["filledTotal"].(string), 64) + if err != nil { + return err + } + left, err = strconv.ParseFloat(invalidJSON["left"].(string), 64) + if err != nil { + return err + } + fee, err = strconv.ParseFloat(invalidJSON["dealFee"].(string), 64) + if err != nil { + return err + } + p := currency.NewPairFromString(invalidJSON["market"].(string)) + var a asset.Item + a, err = g.GetPairAssetType(p) + if err != nil { + return err + } + g.Websocket.DataHandler <- &order.Detail{ + Price: price, + Amount: amount, + ExecutedAmount: filledTotal, + RemainingAmount: left, + Fee: fee, + Exchange: g.Name, + ID: strconv.FormatFloat(invalidJSON["id"].(float64), 'f', -1, 64), + Type: oType, + Side: oSide, + Status: oStatus, + AssetType: a, + Date: time.Unix(cTime, cTimeDec), + LastUpdated: time.Unix(mTime, mTimeDec), + Pair: p, + } + case strings.Contains(result.Method, "depth"): + var IsSnapshot bool + var c string + var data = make(map[string][][]string) + err = json.Unmarshal(result.Params[0], &IsSnapshot) + if err != nil { + return err + } + + err = json.Unmarshal(result.Params[2], &c) + if err != nil { + return err + } + + err = json.Unmarshal(result.Params[1], &data) + if err != nil { + return err + } + + var asks, bids []orderbook.Item + askData, askOk := data["asks"] + for i := range askData { + var amount, price float64 + amount, err = strconv.ParseFloat(askData[i][1], 64) + if err != nil { + return err + } + price, err = strconv.ParseFloat(askData[i][0], 64) + if err != nil { + return err + } + asks = append(asks, orderbook.Item{ + Amount: amount, + Price: price, + }) + } + + bidData, bidOk := data["bids"] + for i := range bidData { + var amount, price float64 + amount, err = strconv.ParseFloat(bidData[i][1], 64) + if err != nil { + return err + } + price, err = strconv.ParseFloat(bidData[i][0], 64) + if err != nil { + return err + } + bids = append(bids, orderbook.Item{ + Amount: amount, + Price: price, + }) + } + + if !askOk && !bidOk { + g.Websocket.DataHandler <- errors.New("gatio websocket error - cannot access ask or bid data") + } + + if IsSnapshot { + if !askOk { + g.Websocket.DataHandler <- errors.New("gatio websocket error - cannot access ask data") + } + + if !bidOk { + g.Websocket.DataHandler <- errors.New("gatio websocket error - cannot access bid data") + } + + var newOrderBook orderbook.Base + newOrderBook.Asks = asks + newOrderBook.Bids = bids + newOrderBook.AssetType = asset.Spot + newOrderBook.Pair = currency.NewPairFromString(c) + newOrderBook.ExchangeName = g.Name + + err = g.Websocket.Orderbook.LoadSnapshot(&newOrderBook) + if err != nil { + return err + } + } else { + err = g.Websocket.Orderbook.Update( + &wsorderbook.WebsocketOrderbookUpdate{ + Asks: asks, + Bids: bids, + Pair: currency.NewPairFromString(c), + UpdateTime: time.Now(), + Asset: asset.Spot, + }) + if err != nil { + return err + } + } + + g.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{ + Pair: currency.NewPairFromString(c), + Asset: asset.Spot, + Exchange: g.Name, + } + + case strings.Contains(result.Method, "kline"): + var data []interface{} + err = json.Unmarshal(result.Params[0], &data) + if err != nil { + return err + } + open, err := strconv.ParseFloat(data[1].(string), 64) + if err != nil { + return err + } + closePrice, err := strconv.ParseFloat(data[2].(string), 64) + if err != nil { + return err + } + high, err := strconv.ParseFloat(data[3].(string), 64) + if err != nil { + return err + } + low, err := strconv.ParseFloat(data[4].(string), 64) + if err != nil { + return err + } + volume, err := strconv.ParseFloat(data[5].(string), 64) + if err != nil { + return err + } + + g.Websocket.DataHandler <- wshandler.KlineData{ + Timestamp: time.Now(), + Pair: currency.NewPairFromString(data[7].(string)), + AssetType: asset.Spot, + Exchange: g.Name, + OpenPrice: open, + ClosePrice: closePrice, + HighPrice: high, + LowPrice: low, + Volume: volume, + } + default: + g.Websocket.DataHandler <- wshandler.UnhandledMessageWarning{Message: g.Name + wshandler.UnhandledMessage + string(respRaw)} + return nil + } + return nil +} + // GenerateAuthenticatedSubscriptions Adds authenticated subscriptions to websocket to be handled by ManageSubscriptions() func (g *Gateio) GenerateAuthenticatedSubscriptions() { if !g.Websocket.CanUseAuthenticatedEndpoints() { diff --git a/exchanges/gateio/gateio_wrapper.go b/exchanges/gateio/gateio_wrapper.go index 9bb85932..7a2a3ba8 100644 --- a/exchanges/gateio/gateio_wrapper.go +++ b/exchanges/gateio/gateio_wrapper.go @@ -422,7 +422,7 @@ func (g *Gateio) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) { } var orderTypeFormat string - if s.OrderSide == order.Buy { + if s.Side == order.Buy { orderTypeFormat = order.Buy.Lower() } else { orderTypeFormat = order.Sell.Lower() @@ -458,12 +458,12 @@ func (g *Gateio) ModifyOrder(action *order.Modify) (string, error) { // CancelOrder cancels an order by its corresponding ID number func (g *Gateio) CancelOrder(order *order.Cancel) error { - orderIDInt, err := strconv.ParseInt(order.OrderID, 10, 64) + orderIDInt, err := strconv.ParseInt(order.ID, 10, 64) if err != nil { return err } _, err = g.CancelExistingOrder(orderIDInt, - g.FormatExchangeCurrency(order.CurrencyPair, order.AssetType).String()) + g.FormatExchangeCurrency(order.Pair, order.AssetType).String()) return err } @@ -508,15 +508,15 @@ func (g *Gateio) GetOrderInfo(orderID string) (order.Detail, error) { orderDetail.RemainingAmount = orders.Orders[x].InitialAmount - orders.Orders[x].FilledAmount orderDetail.ExecutedAmount = orders.Orders[x].FilledAmount orderDetail.Amount = orders.Orders[x].InitialAmount - orderDetail.OrderDate = time.Unix(orders.Orders[x].Timestamp, 0) + orderDetail.Date = time.Unix(orders.Orders[x].Timestamp, 0) orderDetail.Status = order.Status(orders.Orders[x].Status) orderDetail.Price = orders.Orders[x].Rate - orderDetail.CurrencyPair = currency.NewPairDelimiter(orders.Orders[x].CurrencyPair, + orderDetail.Pair = currency.NewPairDelimiter(orders.Orders[x].CurrencyPair, g.GetPairFormat(asset.Spot, false).Delimiter) if strings.EqualFold(orders.Orders[x].Type, order.Ask.String()) { - orderDetail.OrderSide = order.Ask + orderDetail.Side = order.Ask } else if strings.EqualFold(orders.Orders[x].Type, order.Bid.String()) { - orderDetail.OrderSide = order.Buy + orderDetail.Side = order.Buy } return orderDetail, nil } @@ -573,12 +573,12 @@ func (g *Gateio) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) func (g *Gateio) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, error) { var orders []order.Detail var currPair string - if len(req.Currencies) == 1 { - currPair = req.Currencies[0].String() + if len(req.Pairs) == 1 { + currPair = req.Pairs[0].String() } if g.Websocket.CanUseAuthenticatedWebsocketForWrapper() { for i := 0; ; i += 100 { - resp, err := g.wsGetOrderInfo(req.OrderType.String(), i, 100) + resp, err := g.wsGetOrderInfo(req.Type.String(), i, 100) if err != nil { return orders, err } @@ -601,10 +601,10 @@ func (g *Gateio) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, e Exchange: g.Name, AccountID: strconv.FormatInt(resp.WebSocketOrderQueryRecords[j].User, 10), ID: strconv.FormatInt(resp.WebSocketOrderQueryRecords[j].ID, 10), - CurrencyPair: currency.NewPairFromString(resp.WebSocketOrderQueryRecords[j].Market), - OrderSide: orderSide, - OrderType: orderType, - OrderDate: orderDate, + Pair: currency.NewPairFromString(resp.WebSocketOrderQueryRecords[j].Market), + Side: orderSide, + Type: orderType, + Date: orderDate, Price: resp.WebSocketOrderQueryRecords[j].Price, Amount: resp.WebSocketOrderQueryRecords[j].Amount, ExecutedAmount: resp.WebSocketOrderQueryRecords[j].FilledAmount, @@ -636,16 +636,16 @@ func (g *Gateio) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, e Amount: resp.Orders[i].Amount, Price: resp.Orders[i].Rate, RemainingAmount: resp.Orders[i].FilledAmount, - OrderDate: orderDate, - OrderSide: side, + Date: orderDate, + Side: side, Exchange: g.Name, - CurrencyPair: symbol, + Pair: symbol, Status: order.Status(resp.Orders[i].Status), }) } } order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) - order.FilterOrdersBySide(&orders, req.OrderSide) + order.FilterOrdersBySide(&orders, req.Side) return orders, nil } @@ -653,8 +653,8 @@ func (g *Gateio) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, e // Can Limit response to specific order status func (g *Gateio) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail, error) { var trades []TradesResponse - for i := range req.Currencies { - resp, err := g.GetTradeHistory(req.Currencies[i].String()) + for i := range req.Pairs { + resp, err := g.GetTradeHistory(req.Pairs[i].String()) if err != nil { return nil, err } @@ -668,18 +668,18 @@ func (g *Gateio) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail, e side := order.Side(strings.ToUpper(trade.Type)) orderDate := time.Unix(trade.TimeUnix, 0) orders = append(orders, order.Detail{ - ID: strconv.FormatInt(trade.OrderID, 10), - Amount: trade.Amount, - Price: trade.Rate, - OrderDate: orderDate, - OrderSide: side, - Exchange: g.Name, - CurrencyPair: symbol, + ID: strconv.FormatInt(trade.OrderID, 10), + Amount: trade.Amount, + Price: trade.Rate, + Date: orderDate, + Side: side, + Exchange: g.Name, + Pair: symbol, }) } order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) - order.FilterOrdersBySide(&orders, req.OrderSide) + order.FilterOrdersBySide(&orders, req.Side) return orders, nil } diff --git a/exchanges/gemini/gemini_live_test.go b/exchanges/gemini/gemini_live_test.go index 522fda2e..1b75e0dd 100644 --- a/exchanges/gemini/gemini_live_test.go +++ b/exchanges/gemini/gemini_live_test.go @@ -34,6 +34,8 @@ func TestMain(m *testing.M) { log.Fatal("Gemini setup error", err) } g.API.Endpoints.URL = geminiSandboxAPIURL + g.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride() + g.Websocket.TrafficAlert = sharedtestvalues.GetWebsocketStructChannelOverride() log.Printf(sharedtestvalues.LiveTesting, g.Name, g.API.Endpoints.URL) os.Exit(m.Run()) } diff --git a/exchanges/gemini/gemini_mock_test.go b/exchanges/gemini/gemini_mock_test.go index c7982e5c..37a9b91a 100644 --- a/exchanges/gemini/gemini_mock_test.go +++ b/exchanges/gemini/gemini_mock_test.go @@ -45,7 +45,8 @@ func TestMain(m *testing.M) { g.HTTPClient = newClient g.API.Endpoints.URL = serverDetails - + g.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride() + g.Websocket.TrafficAlert = sharedtestvalues.GetWebsocketStructChannelOverride() log.Printf(sharedtestvalues.MockTesting, g.Name, g.API.Endpoints.URL) os.Exit(m.Run()) } diff --git a/exchanges/gemini/gemini_test.go b/exchanges/gemini/gemini_test.go index 2acbfe24..1e4c509d 100644 --- a/exchanges/gemini/gemini_test.go +++ b/exchanges/gemini/gemini_test.go @@ -349,8 +349,8 @@ func TestFormatWithdrawPermissions(t *testing.T) { func TestGetActiveOrders(t *testing.T) { t.Parallel() var getOrdersRequest = order.GetOrdersRequest{ - OrderType: order.AnyType, - Currencies: []currency.Pair{ + Type: order.AnyType, + Pairs: []currency.Pair{ currency.NewPair(currency.LTC, currency.BTC), }, } @@ -369,8 +369,8 @@ func TestGetActiveOrders(t *testing.T) { func TestGetOrderHistory(t *testing.T) { t.Parallel() var getOrdersRequest = order.GetOrdersRequest{ - OrderType: order.AnyType, - Currencies: []currency.Pair{currency.NewPair(currency.LTC, currency.BTC)}, + Type: order.AnyType, + Pairs: []currency.Pair{currency.NewPair(currency.LTC, currency.BTC)}, } _, err := g.GetOrderHistory(&getOrdersRequest) @@ -402,11 +402,11 @@ func TestSubmitOrder(t *testing.T) { Base: currency.LTC, Quote: currency.BTC, }, - OrderSide: order.Buy, - OrderType: order.Limit, - Price: 10, - Amount: 1, - ClientID: "1234234", + Side: order.Buy, + Type: order.Limit, + Price: 10, + Amount: 1, + ClientID: "1234234", } response, err := g.SubmitOrder(orderSubmission) @@ -426,7 +426,7 @@ func TestCancelExchangeOrder(t *testing.T) { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } var orderCancellation = &order.Cancel{ - OrderID: "266029865", + ID: "266029865", } err := g.CancelOrder(orderCancellation) @@ -448,10 +448,10 @@ func TestCancelAllExchangeOrders(t *testing.T) { currencyPair := currency.NewPair(currency.LTC, currency.BTC) var orderCancellation = &order.Cancel{ - OrderID: "1", + ID: "1", WalletAddress: core.BitcoinDonationAddress, AccountID: "1", - CurrencyPair: currencyPair, + Pair: currencyPair, } resp, err := g.CancelAllOrders(orderCancellation) @@ -553,7 +553,7 @@ func TestWsAuth(t *testing.T) { t.Skip(wshandler.WebsocketNotEnabled) } var dialer websocket.Dialer - go g.WsHandleData() + go g.wsReadData() err := g.WsSecureSubscribe(&dialer, geminiWsOrderEvents) if err != nil { t.Error(err) @@ -569,3 +569,495 @@ func TestWsAuth(t *testing.T) { } timer.Stop() } + +func TestWsMissingRole(t *testing.T) { + pressXToJSON := []byte(`{ + "result":"error", + "reason":"MissingRole", + "message":"To access this endpoint, you need to log in to the website and go to the settings page to assign one of these roles [FundManager] to API key wujB3szN54gtJ4QDhqRJ which currently has roles [Trader]" + }`) + err := g.wsHandleData(pressXToJSON, currency.NewPairFromString("BTCUSD")) + if err == nil { + t.Error("Expected error") + } +} + +func TestWsOrderEventSubscriptionResponse(t *testing.T) { + pressXToJSON := []byte(`[ { + "type" : "accepted", + "order_id" : "372456298", + "event_id" : "372456299", + "client_order_id": "20170208_example", + "api_session" : "AeRLptFXoYEqLaNiRwv8", + "symbol" : "btcusd", + "side" : "buy", + "order_type" : "exchange limit", + "timestamp" : "1478203017", + "timestampms" : 1478203017455, + "is_live" : true, + "is_cancelled" : false, + "is_hidden" : false, + "avg_execution_price" : "0", + "original_amount" : "14.0296", + "price" : "1059.54" +} ]`) + err := g.wsHandleData(pressXToJSON, currency.NewPairFromString("BTCUSD")) + if err != nil { + t.Error(err) + } + + pressXToJSON = []byte(`[{ + "type": "accepted", + "order_id": "109535951", + "event_id": "109535952", + "api_session": "UI", + "symbol": "btcusd", + "side": "buy", + "order_type": "exchange limit", + "timestamp": "1547742904", + "timestampms": 1547742904989, + "is_live": true, + "is_cancelled": false, + "is_hidden": false, + "original_amount": "1", + "price": "3592.00", + "socket_sequence": 13 +}]`) + err = g.wsHandleData(pressXToJSON, currency.NewPairFromString("BTCUSD")) + if err != nil { + t.Error(err) + } + + pressXToJSON = []byte(`[{ + "type": "accepted", + "order_id": "109964529", + "event_id": "109964530", + "api_session": "UI", + "symbol": "btcusd", + "side": "buy", + "order_type": "market buy", + "timestamp": "1547756076", + "timestampms": 1547756076644, + "is_live": false, + "is_cancelled": false, + "is_hidden": false, + "total_spend": "200.00", + "socket_sequence": 29 +}]`) + err = g.wsHandleData(pressXToJSON, currency.NewPairFromString("BTCUSD")) + if err != nil { + t.Error(err) + } + + pressXToJSON = []byte(`[{ + "type": "accepted", + "order_id": "109964616", + "event_id": "109964617", + "api_session": "UI", + "symbol": "btcusd", + "side": "sell", + "order_type": "market sell", + "timestamp": "1547756893", + "timestampms": 1547756893937, + "is_live": true, + "is_cancelled": false, + "is_hidden": false, + "original_amount": "25", + "socket_sequence": 26 +}]`) + err = g.wsHandleData(pressXToJSON, currency.NewPairFromString("BTCUSD")) + if err != nil { + t.Error(err) + } + + pressXToJSON = []byte(`[ { + "type" : "accepted", + "order_id" : "6321", + "event_id" : "6322", + "api_session" : "UI", + "symbol" : "btcusd", + "side" : "sell", + "order_type" : "block_trade", + "timestamp" : "1478204198", + "timestampms" : 1478204198989, + "is_live" : true, + "is_cancelled" : false, + "is_hidden" : true, + "avg_execution_price" : "0", + "original_amount" : "500", + "socket_sequence" : 32307 +} ]`) + err = g.wsHandleData(pressXToJSON, currency.NewPairFromString("BTCUSD")) + if err != nil { + t.Error(err) + } +} + +func TestWsSubAck(t *testing.T) { + pressXToJSON := []byte(`{ + "type": "subscription_ack", + "accountId": 5365, + "subscriptionId": "ws-order-events-5365-b8bk32clqeb13g9tk8p0", + "symbolFilter": [ + "btcusd" + ], + "apiSessionFilter": [ + "UI" + ], + "eventTypeFilter": [ + "fill", + "closed" + ] +}`) + err := g.wsHandleData(pressXToJSON, currency.NewPairFromString("BTCUSD")) + if err != nil { + t.Error(err) + } +} + +func TestWsHeartbeat(t *testing.T) { + pressXToJSON := []byte(`{ + "type": "heartbeat", + "timestampms": 1547742998508, + "sequence": 31, + "trace_id": "b8biknoqppr32kc7gfgg", + "socket_sequence": 37 +}`) + err := g.wsHandleData(pressXToJSON, currency.NewPairFromString("BTCUSD")) + if err != nil { + t.Error(err) + } +} + +func TestWsUnsubscribe(t *testing.T) { + pressXToJSON := []byte(`{ + "type": "unsubscribe", + "subscriptions": [{ + "name": "l2", + "symbols": [ + "BTCUSD", + "ETHBTC" + ]}, + {"name": "candles_1m", + "symbols": [ + "BTCUSD", + "ETHBTC" + ]} + ] +}`) + err := g.wsHandleData(pressXToJSON, currency.NewPairFromString("BTCUSD")) + if err != nil { + t.Error(err) + } +} + +func TestWsTradeData(t *testing.T) { + pressXToJSON := []byte(`{ + "type": "update", + "eventId": 5375547515, + "timestamp": 1547760288, + "timestampms": 1547760288001, + "socket_sequence": 15, + "events": [ + { + "type": "trade", + "tid": 5375547515, + "price": "3632.54", + "amount": "0.1362819142", + "makerSide": "ask" + } + ] +}`) + err := g.wsHandleData(pressXToJSON, currency.NewPairFromString("BTCUSD")) + if err != nil { + t.Error(err) + } +} + +func TestWsAuctionData(t *testing.T) { + pressXToJSON := []byte(`{ + "eventId": 371469414, + "socket_sequence":4009, + "timestamp":1486501200, + "timestampms":1486501200000, + "events": [ + { + "amount": "1406", + "makerSide": "auction", + "price": "1048.75", + "tid": 371469414, + "type": "trade" + }, + { + "auction_price": "1048.75", + "auction_quantity": "1406", + "eid": 371469414, + "highest_bid_price": "1050.98", + "lowest_ask_price": "1050.99", + "result": "success", + "time_ms": 1486501200000, + "type": "auction_result" + } + ], + "type": "update" +}`) + err := g.wsHandleData(pressXToJSON, currency.NewPairFromString("BTCUSD")) + if err != nil { + t.Error(err) + } +} + +func TestWsBlockTrade(t *testing.T) { + pressXToJSON := []byte(`{ + "type":"update", + "eventId":1111597035, + "socket_sequence":8, + "timestamp":1501175027, + "timestampms":1501175027304, + "events":[ + { + "type":"block_trade", + "tid":1111597035, + "price":"10100.00", + "amount":"1000" + } + ] +}`) + err := g.wsHandleData(pressXToJSON, currency.NewPairFromString("BTCUSD")) + if err != nil { + t.Error(err) + } +} + +func TestWsCandles(t *testing.T) { + pressXToJSON := []byte(`{ + "type": "candles_15m_updates", + "symbol": "BTCUSD", + "changes": [ + [ + 1561054500000, + 9350.18, + 9358.35, + 9350.18, + 9355.51, + 2.07 + ], + [ + 1561053600000, + 9357.33, + 9357.33, + 9350.18, + 9350.18, + 1.5900161 + ] + ] +}`) + err := g.wsHandleData(pressXToJSON, currency.NewPairFromString("BTCUSD")) + if err != nil { + t.Error(err) + } +} + +func TestWsAuctions(t *testing.T) { + pressXToJSON := []byte(`{ + "eventId": 372481811, + "socket_sequence":23, + "timestamp": 1486591200, + "timestampms": 1486591200000, + "events": [ + { + "auction_open_ms": 1486591200000, + "auction_time_ms": 1486674000000, + "first_indicative_ms": 1486673400000, + "last_cancel_time_ms": 1486673985000, + "type": "auction_open" + } + ], + "type": "update" +}`) + err := g.wsHandleData(pressXToJSON, currency.NewPairFromString("BTCUSD")) + if err != nil { + t.Error(err) + } + + pressXToJSON = []byte(`{ + "type": "update", + "eventId": 2248762586, + "timestamp": 1510865640, + "timestampms": 1510865640122, + "socket_sequence": 177, + "events": [ + { + "type": "auction_indicative", + "eid": 2248762586, + "result": "success", + "time_ms": 1510865640000, + "highest_bid_price": "7730.69", + "lowest_ask_price": "7730.7", + "collar_price": "7730.695", + "indicative_price": "7750", + "indicative_quantity": "45.43325086" + } + ] +}`) + err = g.wsHandleData(pressXToJSON, currency.NewPairFromString("BTCUSD")) + if err != nil { + t.Error(err) + } + + pressXToJSON = []byte(`{ + "type": "update", + "eventId": 2248795680, + "timestamp": 1510866000, + "timestampms": 1510866000095, + "socket_sequence": 2920, + "events": [ + { + "type": "trade", + "tid": 2248795680, + "price": "7763.23", + "amount": "55.95", + "makerSide": "auction" + }, + { + "type": "auction_result", + "eid": 2248795680, + "result": "success", + "time_ms": 1510866000000, + "highest_bid_price": "7769", + "lowest_ask_price": "7769.01", + "collar_price": "7769.005", + "auction_price": "7763.23", + "auction_quantity": "55.95" + } + ] +}`) + err = g.wsHandleData(pressXToJSON, currency.NewPairFromString("BTCUSD")) + if err != nil { + t.Error(err) + } +} + +func TestWsMarketData(t *testing.T) { + pressXToJSON := []byte(`{ + "type": "update", + "eventId": 5375461993, + "socket_sequence": 0, + "events": [ + { + "type": "change", + "reason": "initial", + "price": "3641.61", + "delta": "0.83372051", + "remaining": "0.83372051", + "side": "bid" + }, + { + "type": "change", + "reason": "initial", + "price": "3641.62", + "delta": "4.072", + "remaining": "4.072", + "side": "ask" + } + ] +} `) + err := g.wsHandleData(pressXToJSON, currency.NewPairFromString("BTCUSD")) + if err != nil { + t.Error(err) + } + + pressXToJSON = []byte(`{ + "type": "update", + "eventId": 5375461993, + "socket_sequence": 0, + "events": [ + { + "type": "change", + "reason": "initial", + "price": "3641.61", + "delta": "0.83372051", + "remaining": "0.83372051", + "side": "bid" + }, + { + "type": "change", + "reason": "initial", + "price": "3641.62", + "delta": "4.072", + "remaining": "4.072", + "side": "ask" + } + ] +} `) + err = g.wsHandleData(pressXToJSON, currency.NewPairFromString("BTCUSD")) + if err != nil { + t.Error(err) + } + + pressXToJSON = []byte(`{ + "type": "update", + "eventId": 5375503736, + "timestamp": 1547759964, + "timestampms": 1547759964051, + "socket_sequence": 2, + "events": [ + { + "type": "change", + "side": "bid", + "price": "3628.01", + "remaining": "0", + "delta": "-2", + "reason": "cancel" + } + ] +} `) + err = g.wsHandleData(pressXToJSON, currency.NewPairFromString("BTCUSD")) + if err != nil { + t.Error(err) + } +} + +func TestResponseToStatus(t *testing.T) { + type TestCases struct { + Case string + Result order.Status + } + testCases := []TestCases{ + {Case: "accepted", Result: order.New}, + {Case: "booked", Result: order.Active}, + {Case: "fill", Result: order.Filled}, + {Case: "cancelled", Result: order.Cancelled}, + {Case: "cancel_rejected", Result: order.Rejected}, + {Case: "closed", Result: order.Filled}, + {Case: "LOL", Result: order.UnknownStatus}, + } + for i := range testCases { + result, _ := stringToOrderStatus(testCases[i].Case) + if result != testCases[i].Result { + t.Errorf("Exepcted: %v, received: %v", testCases[i].Result, result) + } + } +} + +func TestResponseToOrderType(t *testing.T) { + type TestCases struct { + Case string + Result order.Type + } + testCases := []TestCases{ + {Case: "exchange limit", Result: order.Limit}, + {Case: "auction-only limit", Result: order.Limit}, + {Case: "indication-of-interest limit", Result: order.Limit}, + {Case: "market buy", Result: order.Market}, + {Case: "market sell", Result: order.Market}, + {Case: "block_trade", Result: order.Market}, + {Case: "LOL", Result: order.UnknownType}, + } + for i := range testCases { + result, _ := stringToOrderType(testCases[i].Case) + if result != testCases[i].Result { + t.Errorf("Exepcted: %v, received: %v", testCases[i].Result, result) + } + } +} diff --git a/exchanges/gemini/gemini_types.go b/exchanges/gemini/gemini_types.go index de687b43..abea9703 100644 --- a/exchanges/gemini/gemini_types.go +++ b/exchanges/gemini/gemini_types.go @@ -275,87 +275,29 @@ type WsHeartbeatResponse struct { SocketSequence int64 `json:"socket_sequence"` } -// WsActiveOrdersResponse contains active orders -type WsActiveOrdersResponse struct { - Type string `json:"type"` - OrderID string `json:"order_id"` - APISession string `json:"api_session"` - Symbol currency.Pair `json:"symbol"` - Side string `json:"side"` - OrderType string `json:"order_type"` - Timestamp string `json:"timestamp"` - Timestampms int64 `json:"timestampms"` - IsLive bool `json:"is_live"` - IsCancelled bool `json:"is_cancelled"` - IsHidden bool `json:"is_hidden"` - AvgExecutionPrice float64 `json:"avg_execution_price,string"` - ExecutedAmount float64 `json:"executed_amount,string"` - RemainingAmount float64 `json:"remaining_amount,string"` - OriginalAmount float64 `json:"original_amount,string"` - Price float64 `json:"price,string"` - SocketSequence int64 `json:"socket_sequence"` -} - -// WsOrderRejectedResponse ws response -type WsOrderRejectedResponse struct { - Type string `json:"type"` - OrderID string `json:"order_id"` - EventID string `json:"event_id"` - Reason string `json:"reason"` - APISession string `json:"api_session"` - Symbol currency.Pair `json:"symbol"` - Side string `json:"side"` - OrderType string `json:"order_type"` - Timestamp string `json:"timestamp"` - Timestampms int64 `json:"timestampms"` - IsLive bool `json:"is_live"` - OriginalAmount float64 `json:"original_amount,string"` - Price float64 `json:"price,string"` - SocketSequence int64 `json:"socket_sequence"` -} - -// WsOrderBookedResponse ws response -type WsOrderBookedResponse struct { - Type string `json:"type"` - OrderID string `json:"order_id"` - EventID string `json:"event_id"` - APISession string `json:"api_session"` - Symbol currency.Pair `json:"symbol"` - Side string `json:"side"` - OrderType string `json:"order_type"` - Timestamp string `json:"timestamp"` - Timestampms int64 `json:"timestampms"` - IsLive bool `json:"is_live"` - IsCancelled bool `json:"is_cancelled"` - IsHidden bool `json:"is_hidden"` - AvgExecutionPrice float64 `json:"avg_execution_price,string"` - ExecutedAmount float64 `json:"executed_amount,string"` - RemainingAmount float64 `json:"remaining_amount,string"` - OriginalAmount float64 `json:"original_amount,string"` - Price float64 `json:"price,string"` - SocketSequence int64 `json:"socket_sequence"` -} - -// WsOrderFilledResponse ws response -type WsOrderFilledResponse struct { - Type string `json:"type"` - OrderID string `json:"order_id"` - APISession string `json:"api_session"` - Symbol currency.Pair `json:"symbol"` - Side string `json:"side"` - OrderType string `json:"order_type"` - Timestamp string `json:"timestamp"` - Timestampms int64 `json:"timestampms"` +// WsOrderResponse contains active orders +type WsOrderResponse struct { IsLive bool `json:"is_live"` IsCancelled bool `json:"is_cancelled"` IsHidden bool `json:"is_hidden"` + SocketSequence int64 `json:"socket_sequence"` + Timestampms int64 `json:"timestampms"` AvgExecutionPrice float64 `json:"avg_execution_price,string"` ExecutedAmount float64 `json:"executed_amount,string"` RemainingAmount float64 `json:"remaining_amount,string"` OriginalAmount float64 `json:"original_amount,string"` Price float64 `json:"price,string"` + EventID string `json:"event_id"` + CancelCommandID string `json:"cancel_command_id"` + Reason string `json:"reason"` + Type string `json:"type"` + OrderID string `json:"order_id"` + APISession string `json:"api_session"` + Symbol string `json:"symbol"` + Side string `json:"side"` + OrderType string `json:"order_type"` + Timestamp string `json:"timestamp"` Fill WsOrderFilledData `json:"fill"` - SocketSequence int64 `json:"socket_sequence"` } // WsOrderFilledData ws response data @@ -368,72 +310,16 @@ type WsOrderFilledData struct { FeeCurrency string `json:"fee_currency"` } -// WsOrderCancelledResponse ws response -type WsOrderCancelledResponse struct { - Type string `json:"type"` - OrderID string `json:"order_id"` - EventID string `json:"event_id"` - CancelCommandID string `json:"cancel_command_id,omitempty"` - Reason string `json:"reason"` - APISession string `json:"api_session"` - Symbol currency.Pair `json:"symbol"` - Side string `json:"side"` - OrderType string `json:"order_type"` - Timestamp string `json:"timestamp"` - Timestampms int64 `json:"timestampms"` - IsLive bool `json:"is_live"` - IsCancelled bool `json:"is_cancelled"` - IsHidden bool `json:"is_hidden"` - AvgExecutionPrice float64 `json:"avg_execution_price,string"` - ExecutedAmount float64 `json:"executed_amount,string"` - RemainingAmount float64 `json:"remaining_amount,string"` - OriginalAmount float64 `json:"original_amount,string"` - Price float64 `json:"price,string"` - SocketSequence int64 `json:"socket_sequence"` +type wsUnsubscribeResponse struct { + Type string `json:"type"` + Subscriptions []struct { + Name string `json:"name"` + Symbols []string `json:"symbols"` + } `json:"subscriptions"` } -// WsOrderCancellationRejectedResponse ws response -type WsOrderCancellationRejectedResponse struct { - Type string `json:"type"` - OrderID string `json:"order_id"` - EventID string `json:"event_id"` - CancelCommandID string `json:"cancel_command_id"` - Reason string `json:"reason"` - APISession string `json:"api_session"` - Symbol currency.Pair `json:"symbol"` - Side string `json:"side"` - OrderType string `json:"order_type"` - Timestamp string `json:"timestamp"` - Timestampms int64 `json:"timestampms"` - IsLive bool `json:"is_live"` - IsCancelled bool `json:"is_cancelled"` - IsHidden bool `json:"is_hidden"` - AvgExecutionPrice float64 `json:"avg_execution_price,string"` - ExecutedAmount float64 `json:"executed_amount,string"` - RemainingAmount float64 `json:"remaining_amount,string"` - OriginalAmount float64 `json:"original_amount,string"` - Price float64 `json:"price,string"` - SocketSequence int64 `json:"socket_sequence"` -} - -// WsOrderClosedResponse ws response -type WsOrderClosedResponse struct { - Type string `json:"type"` - OrderID string `json:"order_id"` - EventID string `json:"event_id"` - APISession string `json:"api_session"` - Symbol currency.Pair `json:"symbol"` - Side string `json:"side"` - OrderType string `json:"order_type"` - Timestamp string `json:"timestamp"` - Timestampms int64 `json:"timestampms"` - IsLive bool `json:"is_live"` - IsCancelled bool `json:"is_cancelled"` - IsHidden bool `json:"is_hidden"` - AvgExecutionPrice float64 `json:"avg_execution_price,string"` - ExecutedAmount float64 `json:"executed_amount,string"` - RemainingAmount float64 `json:"remaining_amount,string"` - OriginalAmount float64 `json:"original_amount,string"` - Price float64 `json:"price,string"` - SocketSequence int64 `json:"socket_sequence"` +type wsCandleResponse struct { + Type string `json:"type"` + Symbol string `json:"symbol"` + Changes [][]float64 `json:"changes"` } diff --git a/exchanges/gemini/gemini_websocket.go b/exchanges/gemini/gemini_websocket.go index 1f0122dd..af5d0e34 100644 --- a/exchanges/gemini/gemini_websocket.go +++ b/exchanges/gemini/gemini_websocket.go @@ -51,7 +51,7 @@ func (g *Gemini) WsConnect() error { dialer.Proxy = http.ProxyURL(proxy) } - go g.WsHandleData() + go g.wsReadData() err := g.WsSecureSubscribe(&dialer, geminiWsOrderEvents) if err != nil { log.Errorf(log.ExchangeSys, "%v - authentication failed: %v\n", g.Name, err) @@ -82,7 +82,7 @@ func (g *Gemini) WsSubscribe(dialer *websocket.Dialer) error { return fmt.Errorf("%v Websocket connection %v error. Error %v", g.Name, endpoint, err) } - go g.WsReadData(connection, enabledCurrencies[i]) + go g.wsFunnelConnectionData(connection, enabledCurrencies[i]) if len(enabledCurrencies)-1 == i { return nil } @@ -126,13 +126,12 @@ func (g *Gemini) WsSecureSubscribe(dialer *websocket.Dialer, url string) error { if err != nil { return fmt.Errorf("%v Websocket connection %v error. Error %v", g.Name, endpoint, err) } - go g.WsReadData(g.AuthenticatedWebsocketConn, currency.Pair{}) + go g.wsFunnelConnectionData(g.AuthenticatedWebsocketConn, currency.Pair{}) return nil } -// WsReadData reads from the websocket connection and returns the websocket -// response -func (g *Gemini) WsReadData(ws *wshandler.WebsocketConnection, c currency.Pair) { +// wsFunnelConnectionData receives data from multiple connections and passes it to wsReadData +func (g *Gemini) wsFunnelConnectionData(ws *wshandler.WebsocketConnection, c currency.Pair) { g.Websocket.Wg.Add(1) defer g.Websocket.Wg.Done() for { @@ -151,9 +150,8 @@ func (g *Gemini) WsReadData(ws *wshandler.WebsocketConnection, c currency.Pair) } } -// WsHandleData handles all the websocket data coming from the websocket -// connection -func (g *Gemini) WsHandleData() { +// wsReadData receives and passes on websocket messages for processing +func (g *Gemini) wsReadData() { g.Websocket.Wg.Add(1) defer g.Websocket.Wg.Done() for { @@ -165,98 +163,199 @@ func (g *Gemini) WsHandleData() { if string(resp.Raw) == "[]" { continue } - var result map[string]interface{} - err := json.Unmarshal(resp.Raw, &result) + err := g.wsHandleData(resp.Raw, resp.Currency) if err != nil { - g.Websocket.DataHandler <- fmt.Errorf("%v Error: %v, Raw: %v", g.Name, err, string(resp.Raw)) - continue - } - switch result["type"] { - case "subscription_ack": - var result WsSubscriptionAcknowledgementResponse - err := json.Unmarshal(resp.Raw, &result) - if err != nil { - g.Websocket.DataHandler <- err - continue - } - g.Websocket.DataHandler <- result - case "initial": - var result WsSubscriptionAcknowledgementResponse - err := json.Unmarshal(resp.Raw, &result) - if err != nil { - g.Websocket.DataHandler <- err - continue - } - g.Websocket.DataHandler <- result - case "accepted": - var result WsActiveOrdersResponse - err := json.Unmarshal(resp.Raw, &result) - if err != nil { - g.Websocket.DataHandler <- err - continue - } - g.Websocket.DataHandler <- result - case "booked": - var result WsOrderBookedResponse - err := json.Unmarshal(resp.Raw, &result) - if err != nil { - g.Websocket.DataHandler <- err - continue - } - g.Websocket.DataHandler <- result - case "fill": - var result WsOrderFilledResponse - err := json.Unmarshal(resp.Raw, &result) - if err != nil { - g.Websocket.DataHandler <- err - continue - } - g.Websocket.DataHandler <- result - case "cancelled": - var result WsOrderCancelledResponse - err := json.Unmarshal(resp.Raw, &result) - if err != nil { - g.Websocket.DataHandler <- err - continue - } - g.Websocket.DataHandler <- result - case "closed": - var result WsOrderClosedResponse - err := json.Unmarshal(resp.Raw, &result) - if err != nil { - g.Websocket.DataHandler <- err - continue - } - g.Websocket.DataHandler <- result - case "heartbeat": - var result WsHeartbeatResponse - err := json.Unmarshal(resp.Raw, &result) - if err != nil { - g.Websocket.DataHandler <- err - continue - } - g.Websocket.DataHandler <- result - case "update": - if resp.Currency.IsEmpty() { - g.Websocket.DataHandler <- fmt.Errorf("%v - unhandled data %s", - g.Name, resp.Raw) - continue - } - var marketUpdate WsMarketUpdateResponse - err := json.Unmarshal(resp.Raw, &marketUpdate) - if err != nil { - g.Websocket.DataHandler <- err - continue - } - g.wsProcessUpdate(marketUpdate, resp.Currency) - default: - g.Websocket.DataHandler <- fmt.Errorf("%v - unhandled data %s", - g.Name, resp.Raw) + g.Websocket.DataHandler <- err } } } } +func (g *Gemini) wsHandleData(respRaw []byte, curr currency.Pair) error { + // only order details are sent in arrays + if strings.HasPrefix(string(respRaw), "[") { + var result []WsOrderResponse + err := json.Unmarshal(respRaw, &result) + if err != nil { + return err + } + + for i := range result { + oSide, err := order.StringToOrderSide(result[i].Side) + if err != nil { + g.Websocket.DataHandler <- order.ClassificationError{ + Exchange: g.Name, + OrderID: result[i].OrderID, + Err: err, + } + } + var oType order.Type + oType, err = stringToOrderType(result[i].OrderType) + if err != nil { + g.Websocket.DataHandler <- order.ClassificationError{ + Exchange: g.Name, + OrderID: result[i].OrderID, + Err: err, + } + } + var oStatus order.Status + oStatus, err = stringToOrderStatus(result[i].Type) + if err != nil { + g.Websocket.DataHandler <- order.ClassificationError{ + Exchange: g.Name, + OrderID: result[i].OrderID, + Err: err, + } + } + p := currency.NewPairFromString(result[i].Symbol) + var a asset.Item + a, err = g.GetPairAssetType(p) + if err != nil { + return err + } + g.Websocket.DataHandler <- &order.Detail{ + HiddenOrder: result[i].IsHidden, + Price: result[i].Price, + Amount: result[i].OriginalAmount, + ExecutedAmount: result[i].ExecutedAmount, + RemainingAmount: result[i].RemainingAmount, + Exchange: g.Name, + ID: result[i].OrderID, + Type: oType, + Side: oSide, + Status: oStatus, + AssetType: a, + Date: time.Unix(0, result[i].Timestampms*int64(time.Millisecond)), + Pair: p, + } + } + return nil + } + var result map[string]interface{} + err := json.Unmarshal(respRaw, &result) + if err != nil { + return fmt.Errorf("%v Error: %v, Raw: %v", g.Name, err, string(respRaw)) + } + if _, ok := result["type"]; ok { + switch result["type"] { + case "subscription_ack": + var result WsSubscriptionAcknowledgementResponse + err := json.Unmarshal(respRaw, &result) + if err != nil { + return err + } + g.Websocket.DataHandler <- result + case "unsubscribe": + var result wsUnsubscribeResponse + err := json.Unmarshal(respRaw, &result) + if err != nil { + return err + } + g.Websocket.DataHandler <- result + case "initial": + var result WsSubscriptionAcknowledgementResponse + err := json.Unmarshal(respRaw, &result) + if err != nil { + return err + } + g.Websocket.DataHandler <- result + case "heartbeat": + var result WsHeartbeatResponse + err := json.Unmarshal(respRaw, &result) + if err != nil { + return err + } + g.Websocket.DataHandler <- result + case "update": + if curr.IsEmpty() { + return fmt.Errorf("%v - `update` response error. Currency is empty %s", + g.Name, respRaw) + } + var marketUpdate WsMarketUpdateResponse + err := json.Unmarshal(respRaw, &marketUpdate) + if err != nil { + return err + } + g.wsProcessUpdate(marketUpdate, curr) + case "candles_1m_updates", + "candles_5m_updates", + "candles_15m_updates", + "candles_30m_updates", + "candles_1h_updates", + "candles_6h_updates", + "candles_1d_updates": + var candle wsCandleResponse + err := json.Unmarshal(respRaw, &result) + if err != nil { + return err + } + for i := range candle.Changes { + g.Websocket.DataHandler <- wshandler.KlineData{ + Timestamp: time.Unix(int64(candle.Changes[i][0])*1000, 0), + Pair: curr, + AssetType: asset.Spot, + Exchange: g.Name, + Interval: result["type"].(string), + OpenPrice: candle.Changes[i][1], + ClosePrice: candle.Changes[i][4], + HighPrice: candle.Changes[i][2], + LowPrice: candle.Changes[i][3], + Volume: candle.Changes[i][5], + } + } + + default: + g.Websocket.DataHandler <- wshandler.UnhandledMessageWarning{Message: g.Name + wshandler.UnhandledMessage + string(respRaw)} + return nil + } + } else if _, ok := result["result"]; ok { + switch result["result"].(string) { + case "error": + if _, ok := result["reason"]; ok { + if _, ok := result["message"]; ok { + return errors.New(result["reason"].(string) + " - " + result["message"].(string)) + } + } + return fmt.Errorf("%v Unhandled websocket error %s", g.Name, respRaw) + default: + g.Websocket.DataHandler <- wshandler.UnhandledMessageWarning{Message: g.Name + wshandler.UnhandledMessage + string(respRaw)} + return nil + } + } + return nil +} + +func stringToOrderStatus(status string) (order.Status, error) { + switch status { + case "accepted": + return order.New, nil + case "booked": + return order.Active, nil + case "fill": + return order.Filled, nil + case "cancelled": + return order.Cancelled, nil + case "cancel_rejected": + return order.Rejected, nil + case "closed": + return order.Filled, nil + default: + return order.UnknownStatus, errors.New(status + " not recognised as order status") + } +} + +func stringToOrderType(oType string) (order.Type, error) { + switch oType { + case "exchange limit", "auction-only limit", "indication-of-interest limit": + return order.Limit, nil + case "market buy", "market sell", "block_trade": + // block trades are conducted off order-book, so their type is market, but would be considered a hidden trade + return order.Market, nil + default: + return order.UnknownType, errors.New(oType + " not recognised as order type") + } +} + // wsProcessUpdate handles order book data func (g *Gemini) wsProcessUpdate(result WsMarketUpdateResponse, pair currency.Pair) { if result.Timestamp == 0 && result.TimestampMS == 0 { @@ -295,7 +394,15 @@ func (g *Gemini) wsProcessUpdate(result WsMarketUpdateResponse, pair currency.Pa } else { var asks, bids []orderbook.Item for i := range result.Events { - if result.Events[i].Type == "trade" { + switch result.Events[i].Type { + case "trade": + tSide, err := order.StringToOrderSide(result.Events[i].MakerSide) + if err != nil { + g.Websocket.DataHandler <- order.ClassificationError{ + Exchange: g.Name, + Err: err, + } + } g.Websocket.DataHandler <- wshandler.TradeData{ Timestamp: time.Unix(0, result.Timestamp), CurrencyPair: pair, @@ -303,9 +410,9 @@ func (g *Gemini) wsProcessUpdate(result WsMarketUpdateResponse, pair currency.Pa Exchange: g.Name, Price: result.Events[i].Price, Amount: result.Events[i].Amount, - Side: result.Events[i].MakerSide, + Side: tSide, } - } else { + case "change": item := orderbook.Item{ Amount: result.Events[i].Remaining, Price: result.Events[i].Price, @@ -315,8 +422,13 @@ func (g *Gemini) wsProcessUpdate(result WsMarketUpdateResponse, pair currency.Pa } else { bids = append(bids, item) } + default: + g.Websocket.DataHandler <- fmt.Errorf("%s - Unhandled websocket update: %+v", g.Name, result) } } + if len(asks) == 0 && len(bids) == 0 { + return + } err := g.Websocket.Orderbook.Update(&wsorderbook.WebsocketOrderbookUpdate{ Asks: asks, Bids: bids, diff --git a/exchanges/gemini/gemini_wrapper.go b/exchanges/gemini/gemini_wrapper.go index 611ec8d2..9138a12d 100644 --- a/exchanges/gemini/gemini_wrapper.go +++ b/exchanges/gemini/gemini_wrapper.go @@ -94,6 +94,9 @@ func (g *Gemini) SetDefaults() { TradeFetching: true, AuthenticatedEndpoints: true, MessageSequenceNumbers: true, + Subscribe: true, + Unsubscribe: true, + KlineFetching: true, }, WithdrawPermissions: exchange.AutoWithdrawCryptoWithAPIPermission | exchange.AutoWithdrawCryptoWithSetup | @@ -338,7 +341,7 @@ func (g *Gemini) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) { return submitOrderResponse, err } - if s.OrderType != order.Limit { + if s.Type != order.Limit { return submitOrderResponse, errors.New("only limit orders are enabled through this exchange") } @@ -347,7 +350,7 @@ func (g *Gemini) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) { g.FormatExchangeCurrency(s.Pair, asset.Spot).String(), s.Amount, s.Price, - s.OrderSide.String(), + s.Side.String(), "exchange limit") if err != nil { return submitOrderResponse, err @@ -369,7 +372,7 @@ func (g *Gemini) ModifyOrder(action *order.Modify) (string, error) { // CancelOrder cancels an order by its corresponding ID number func (g *Gemini) CancelOrder(order *order.Cancel) error { - orderIDInt, err := strconv.ParseInt(order.OrderID, 10, 64) + orderIDInt, err := strconv.ParseInt(order.ID, 10, 64) if err != nil { return err } @@ -479,31 +482,31 @@ func (g *Gemini) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, e ID: strconv.FormatInt(resp[i].OrderID, 10), ExecutedAmount: resp[i].ExecutedAmount, Exchange: g.Name, - OrderType: orderType, - OrderSide: side, + Type: orderType, + Side: side, Price: resp[i].Price, - CurrencyPair: symbol, - OrderDate: orderDate, + Pair: symbol, + Date: orderDate, }) } order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) - order.FilterOrdersBySide(&orders, req.OrderSide) - order.FilterOrdersByType(&orders, req.OrderType) - order.FilterOrdersByCurrencies(&orders, req.Currencies) + order.FilterOrdersBySide(&orders, req.Side) + order.FilterOrdersByType(&orders, req.Type) + order.FilterOrdersByCurrencies(&orders, req.Pairs) return orders, nil } // GetOrderHistory retrieves account order information // Can Limit response to specific order status func (g *Gemini) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail, error) { - if len(req.Currencies) == 0 { + if len(req.Pairs) == 0 { return nil, errors.New("currency must be supplied") } var trades []TradeHistory - for j := range req.Currencies { - resp, err := g.GetTradeHistory(g.FormatExchangeCurrency(req.Currencies[j], + for j := range req.Pairs { + resp, err := g.GetTradeHistory(g.FormatExchangeCurrency(req.Pairs[j], asset.Spot).String(), req.StartTicks.Unix()) if err != nil { @@ -511,8 +514,8 @@ func (g *Gemini) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail, e } for i := range resp { - resp[i].BaseCurrency = req.Currencies[j].Base.String() - resp[i].QuoteCurrency = req.Currencies[j].Quote.String() + resp[i].BaseCurrency = req.Pairs[j].Base.String() + resp[i].QuoteCurrency = req.Pairs[j].Quote.String() trades = append(trades, resp[i]) } } @@ -523,21 +526,21 @@ func (g *Gemini) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail, e orderDate := time.Unix(trades[i].Timestamp, 0) orders = append(orders, order.Detail{ - Amount: trades[i].Amount, - ID: strconv.FormatInt(trades[i].OrderID, 10), - Exchange: g.Name, - OrderDate: orderDate, - OrderSide: side, - Fee: trades[i].FeeAmount, - Price: trades[i].Price, - CurrencyPair: currency.NewPairWithDelimiter(trades[i].BaseCurrency, + Amount: trades[i].Amount, + ID: strconv.FormatInt(trades[i].OrderID, 10), + Exchange: g.Name, + Date: orderDate, + Side: side, + Fee: trades[i].FeeAmount, + Price: trades[i].Price, + Pair: currency.NewPairWithDelimiter(trades[i].BaseCurrency, trades[i].QuoteCurrency, g.GetPairFormat(asset.Spot, false).Delimiter), }) } order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) - order.FilterOrdersBySide(&orders, req.OrderSide) + order.FilterOrdersBySide(&orders, req.Side) return orders, nil } diff --git a/exchanges/hitbtc/hitbtc_test.go b/exchanges/hitbtc/hitbtc_test.go index 36ebae24..10073462 100644 --- a/exchanges/hitbtc/hitbtc_test.go +++ b/exchanges/hitbtc/hitbtc_test.go @@ -51,6 +51,8 @@ func TestMain(m *testing.M) { log.Fatal("HitBTC setup error", err) } + h.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride() + h.Websocket.TrafficAlert = sharedtestvalues.GetWebsocketStructChannelOverride() os.Exit(m.Run()) } @@ -225,8 +227,8 @@ func TestFormatWithdrawPermissions(t *testing.T) { func TestGetActiveOrders(t *testing.T) { var getOrdersRequest = order.GetOrdersRequest{ - OrderType: order.AnyType, - Currencies: []currency.Pair{currency.NewPair(currency.ETH, currency.BTC)}, + Type: order.AnyType, + Pairs: []currency.Pair{currency.NewPair(currency.ETH, currency.BTC)}, } _, err := h.GetActiveOrders(&getOrdersRequest) @@ -239,8 +241,8 @@ func TestGetActiveOrders(t *testing.T) { func TestGetOrderHistory(t *testing.T) { var getOrdersRequest = order.GetOrdersRequest{ - OrderType: order.AnyType, - Currencies: []currency.Pair{currency.NewPair(currency.ETH, currency.BTC)}, + Type: order.AnyType, + Pairs: []currency.Pair{currency.NewPair(currency.ETH, currency.BTC)}, } _, err := h.GetOrderHistory(&getOrdersRequest) @@ -267,11 +269,11 @@ func TestSubmitOrder(t *testing.T) { Base: currency.DGD, Quote: currency.BTC, }, - OrderSide: order.Buy, - OrderType: order.Limit, - Price: 1, - Amount: 1, - ClientID: "meowOrder", + Side: order.Buy, + Type: order.Limit, + Price: 1, + Amount: 1, + ClientID: "meowOrder", } response, err := h.SubmitOrder(orderSubmission) if areTestAPIKeysSet() && (err != nil || !response.IsOrderPlaced) { @@ -288,10 +290,10 @@ func TestCancelExchangeOrder(t *testing.T) { currencyPair := currency.NewPair(currency.LTC, currency.BTC) var orderCancellation = &order.Cancel{ - OrderID: "1", + ID: "1", WalletAddress: core.BitcoinDonationAddress, AccountID: "1", - CurrencyPair: currencyPair, + Pair: currencyPair, } err := h.CancelOrder(orderCancellation) @@ -310,10 +312,10 @@ func TestCancelAllExchangeOrders(t *testing.T) { currencyPair := currency.NewPair(currency.LTC, currency.BTC) var orderCancellation = &order.Cancel{ - OrderID: "1", + ID: "1", WalletAddress: core.BitcoinDonationAddress, AccountID: "1", - CurrencyPair: currencyPair, + Pair: currencyPair, } resp, err := h.CancelAllOrders(orderCancellation) @@ -421,7 +423,7 @@ func setupWsAuth(t *testing.T) { if err != nil { t.Fatal(err) } - go h.WsHandleData() + go h.wsReadData() h.wsLogin() timer := time.NewTimer(time.Second) select { @@ -508,11 +510,411 @@ func TestWsGetSymbols(t *testing.T) { } } -// TestWsGetTradingBalance dials websocket, sends get trading balance request. -func TestSsGetCurrencies(t *testing.T) { +// TestWsGetCurrencies dials websocket, sends get trading balance request. +func TestWsGetCurrencies(t *testing.T) { setupWsAuth(t) _, err := h.wsGetCurrencies(currency.BTC) if err != nil { t.Fatal(err) } } + +func TestWsGetActiveOrdersJSON(t *testing.T) { + pressXToJSON := []byte(`{ + "jsonrpc": "2.0", + "method": "activeOrders", + "params": [ + { + "id": "4345613661", + "clientOrderId": "57d5525562c945448e3cbd559bd068c3", + "symbol": "BTCUSD", + "side": "sell", + "status": "new", + "type": "limit", + "timeInForce": "GTC", + "quantity": "0.013", + "price": "0.100000", + "cumQuantity": "0.000", + "postOnly": false, + "createdAt": "2017-10-20T12:17:12.245Z", + "updatedAt": "2017-10-20T12:17:12.245Z", + "reportType": "status" + } + ] +}`) + err := h.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsGetCurrenciesJSON(t *testing.T) { + pressXToJSON := []byte(`{ + "jsonrpc": "2.0", + "result": { + "id": "ETH", + "fullName": "Ethereum", + "crypto": true, + "payinEnabled": true, + "payinPaymentId": false, + "payinConfirmations": 2, + "payoutEnabled": true, + "payoutIsPaymentId": false, + "transferEnabled": true, + "delisted": false, + "payoutFee": "0.001" + }, + "id": 123 +}`) + err := h.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsGetSymbolsJSON(t *testing.T) { + pressXToJSON := []byte(`{ + "jsonrpc": "2.0", + "result": { + "id": "ETHBTC", + "baseCurrency": "ETH", + "quoteCurrency": "BTC", + "quantityIncrement": "0.001", + "tickSize": "0.000001", + "takeLiquidityRate": "0.001", + "provideLiquidityRate": "-0.0001", + "feeCurrency": "BTC" + }, + "id": 123 +}`) + err := h.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsTicker(t *testing.T) { + pressXToJSON := []byte(`{ + "jsonrpc": "2.0", + "method": "ticker", + "params": { + "ask": "0.054464", + "bid": "0.054463", + "last": "0.054463", + "open": "0.057133", + "low": "0.053615", + "high": "0.057559", + "volume": "33068.346", + "volumeQuote": "1832.687530809", + "timestamp": "2017-10-19T15:45:44.941Z", + "symbol": "BTCUSD" + } +}`) + err := h.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsOrderbook(t *testing.T) { + pressXToJSON := []byte(`{ + "jsonrpc": "2.0", + "method": "snapshotOrderbook", + "params": { + "ask": [ + { + "price": "0.054588", + "size": "0.245" + }, + { + "price": "0.054590", + "size": "0.000" + }, + { + "price": "0.054591", + "size": "2.784" + } + ], + "bid": [ + { + "price": "0.054558", + "size": "0.500" + }, + { + "price": "0.054557", + "size": "0.076" + }, + { + "price": "0.054524", + "size": "7.725" + } + ], + "symbol": "BTCUSD", + "sequence": 8073827, + "timestamp": "2018-11-19T05:00:28.193Z" + } +}`) + err := h.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } + + pressXToJSON = []byte(`{ + "jsonrpc": "2.0", + "method": "updateOrderbook", + "params": { + "ask": [ + { + "price": "0.054590", + "size": "0.000" + }, + { + "price": "0.054591", + "size": "0.000" + } + ], + "bid": [ + { + "price": "0.054504", + "size": "0.000" + } + ], + "symbol": "BTCUSD", + "sequence": 8073830, + "timestamp": "2018-11-19T05:00:28.700Z" + } +}`) + err = h.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsOrderNotification(t *testing.T) { + pressXToJSON := []byte(`{ + "jsonrpc": "2.0", + "method": "report", + "params": { + "id": "4345697765", + "clientOrderId": "53b7cf917963464a811a4af426102c19", + "symbol": "BTCUSD", + "side": "sell", + "status": "filled", + "type": "limit", + "timeInForce": "GTC", + "quantity": "0.001", + "price": "0.053868", + "cumQuantity": "0.001", + "postOnly": false, + "createdAt": "2017-10-20T12:20:05.952Z", + "updatedAt": "2017-10-20T12:20:38.708Z", + "reportType": "trade", + "tradeQuantity": "0.001", + "tradePrice": "0.053868", + "tradeId": 55051694, + "tradeFee": "-0.000000005" + } +}`) + err := h.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsSubmitOrderJSON(t *testing.T) { + pressXToJSON := []byte(`{ + "jsonrpc": "2.0", + "result": { + "id": "4345947689", + "clientOrderId": "57d5525562c945448e3cbd559bd068c4", + "symbol": "BTCUSD", + "side": "sell", + "status": "new", + "type": "limit", + "timeInForce": "GTC", + "quantity": "0.001", + "price": "0.093837", + "cumQuantity": "0.000", + "postOnly": false, + "createdAt": "2017-10-20T12:29:43.166Z", + "updatedAt": "2017-10-20T12:29:43.166Z", + "reportType": "new" + }, + "id": 123 +}`) + err := h.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsCancelOrderJSON(t *testing.T) { + pressXToJSON := []byte(`{ + "jsonrpc": "2.0", + "result": { + "id": "4345947689", + "clientOrderId": "57d5525562c945448e3cbd559bd068c4", + "symbol": "BTCUSD", + "side": "sell", + "status": "canceled", + "type": "limit", + "timeInForce": "GTC", + "quantity": "0.001", + "price": "0.093837", + "cumQuantity": "0.000", + "postOnly": false, + "createdAt": "2017-10-20T12:29:43.166Z", + "updatedAt": "2017-10-20T12:31:26.174Z", + "reportType": "canceled" + }, + "id": 123 +}`) + err := h.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsCancelReplaceJSON(t *testing.T) { + pressXToJSON := []byte(`{ + "jsonrpc": "2.0", + "result": { + "id": "4346371528", + "clientOrderId": "9cbe79cb6f864b71a811402a48d4b5b2", + "symbol": "BTCUSD", + "side": "sell", + "status": "new", + "type": "limit", + "timeInForce": "GTC", + "quantity": "0.002", + "price": "0.083837", + "cumQuantity": "0.000", + "postOnly": false, + "createdAt": "2017-10-20T12:47:07.942Z", + "updatedAt": "2017-10-20T12:50:34.488Z", + "reportType": "replaced", + "originalRequestClientOrderId": "9cbe79cb6f864b71a811402a48d4b5b1" + }, + "id": 123 +}`) + err := h.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsGetTradesRequestResponse(t *testing.T) { + pressXToJSON := []byte(`{ + "jsonrpc": "2.0", + "result": [ + { + "currency": "BCN", + "available": "100.000000000", + "reserved": "0" + }, + { + "currency": "BTC", + "available": "0.013634021", + "reserved": "0" + }, + { + "currency": "ETH", + "available": "0", + "reserved": "0.00200000" + } + ], + "id": 123 +}`) + err := h.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsGetActiveOrdersRequestJSON(t *testing.T) { + pressXToJSON := []byte(`{ + "jsonrpc": "2.0", + "result": [ + { + "id": "4346371528", + "clientOrderId": "9cbe79cb6f864b71a811402a48d4b5b2", + "symbol": "BTCUSD", + "side": "sell", + "status": "new", + "type": "limit", + "timeInForce": "GTC", + "quantity": "0.002", + "price": "0.083837", + "cumQuantity": "0.000", + "postOnly": false, + "createdAt": "2017-10-20T12:47:07.942Z", + "updatedAt": "2017-10-20T12:50:34.488Z", + "reportType": "replaced", + "originalRequestClientOrderId": "9cbe79cb6f864b71a811402a48d4b5b1" + } + ], + "id": 123 +}`) + err := h.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsTrades(t *testing.T) { + pressXToJSON := []byte(`{ + "jsonrpc": "2.0", + "method": "snapshotTrades", + "params": { + "data": [ + { + "id": 54469456, + "price": "0.054656", + "quantity": "0.057", + "side": "buy", + "timestamp": "2017-10-19T16:33:42.821Z" + }, + { + "id": 54469497, + "price": "0.054656", + "quantity": "0.092", + "side": "buy", + "timestamp": "2017-10-19T16:33:48.754Z" + }, + { + "id": 54469697, + "price": "0.054669", + "quantity": "0.002", + "side": "buy", + "timestamp": "2017-10-19T16:34:13.288Z" + } + ], + "symbol": "BTCUSD" + } +}`) + err := h.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } + + pressXToJSON = []byte(`{ + "jsonrpc": "2.0", + "method": "updateTrades", + "params": { + "data": [ + { + "id": 54469813, + "price": "0.054670", + "quantity": "0.183", + "side": "buy", + "timestamp": "2017-10-19T16:34:25.041Z" + } + ], + "symbol": "BTCUSD" + } +} `) + err = h.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} diff --git a/exchanges/hitbtc/hitbtc_types.go b/exchanges/hitbtc/hitbtc_types.go index 104645a1..183c8052 100644 --- a/exchanges/hitbtc/hitbtc_types.go +++ b/exchanges/hitbtc/hitbtc_types.go @@ -52,11 +52,11 @@ type Orderbook struct { // TradeHistory contains trade history data type TradeHistory struct { - ID int64 `json:"id"` // Trade id - Timestamp string `json:"timestamp"` // Trade timestamp - Side string `json:"side"` // Trade side sell or buy - Price float64 `json:"price,string"` // Trade price - Quantity float64 `json:"quantity,string"` // Trade quantity + ID int64 `json:"id"` // Trade id + Timestamp time.Time `json:"timestamp"` // Trade timestamp + Side string `json:"side"` // Trade side sell or buy + Price float64 `json:"price,string"` // Trade price + Quantity float64 `json:"quantity,string"` // Trade quantity } // ChartData contains chart data @@ -322,16 +322,16 @@ type params struct { // WsTicker defines websocket ticker feed return params type WsTicker struct { Params struct { - Ask float64 `json:"ask,string"` - Bid float64 `json:"bid,string"` - Last float64 `json:"last,string"` - Open float64 `json:"open,string"` - Low float64 `json:"low,string"` - High float64 `json:"high,string"` - Volume float64 `json:"volume,string"` - VolumeQuote float64 `json:"volumeQuote,string"` - Timestamp string `json:"timestamp"` - Symbol string `json:"symbol"` + Ask float64 `json:"ask,string"` + Bid float64 `json:"bid,string"` + Last float64 `json:"last,string"` + Open float64 `json:"open,string"` + Low float64 `json:"low,string"` + High float64 `json:"high,string"` + Volume float64 `json:"volume,string"` + VolumeQuote float64 `json:"volumeQuote,string"` + Timestamp time.Time `json:"timestamp"` + Symbol string `json:"symbol"` } `json:"params"` } @@ -355,11 +355,11 @@ type WsOrderbook struct { type WsTrade struct { Params struct { Data []struct { - ID int64 `json:"id"` - Price float64 `json:"price,string"` - Quantity float64 `json:"quantity,string"` - Side string `json:"side"` - Timestamp string `json:"timestamp"` + ID int64 `json:"id"` + Price float64 `json:"price,string"` + Quantity float64 `json:"quantity,string"` + Side string `json:"side"` + Timestamp time.Time `json:"timestamp"` } `json:"data"` Symbol string `json:"symbol"` } `json:"params"` @@ -379,28 +379,48 @@ type WsLoginData struct { Signature string `json:"signature"` } -// WsActiveOrdersResponse Active order response for auth subscription to reports -type WsActiveOrdersResponse struct { - Params []WsActiveOrdersResponseData `json:"params"` - Error ResponseError `json:"error,omitempty"` +// wsActiveOrdersResponse Active order response for auth subscription to reports +type wsActiveOrdersResponse struct { + Params []wsOrderData `json:"params"` + Error ResponseError `json:"error,omitempty"` } -// WsActiveOrdersResponseData Active order data for WsActiveOrdersResponse -type WsActiveOrdersResponseData struct { - ID string `json:"id"` - ClientOrderID string `json:"clientOrderId,omitempty"` - Symbol string `json:"symbol"` - Side string `json:"side"` - Status string `json:"status"` - Type string `json:"type"` - TimeInForce string `json:"timeInForce"` - Quantity float64 `json:"quantity,string"` - Price float64 `json:"price,string"` - CumQuantity float64 `json:"cumQuantity,string"` - PostOnly bool `json:"postOnly"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` - ReportType string `json:"reportType"` +type wsReportResponse struct { + OrderData wsOrderData `json:"params"` + ID int64 `json:"id"` +} + +type wsOrderResponse struct { + OrderData wsOrderData `json:"result"` + ID int64 `json:"id"` +} + +type wsActiveOrderRequestResponse struct { + OrderData []wsOrderData `json:"result"` + ID int64 `json:"id"` +} + +// wsOrderData Active order data for WsActiveOrdersResponse +type wsOrderData struct { + ID string `json:"id"` + ClientOrderID string `json:"clientOrderId,omitempty"` + Symbol string `json:"symbol"` + Side string `json:"side"` + Status string `json:"status"` + Type string `json:"type"` + TimeInForce string `json:"timeInForce"` + Quantity float64 `json:"quantity,string"` + Price float64 `json:"price,string"` + CumQuantity float64 `json:"cumQuantity,string"` + PostOnly bool `json:"postOnly"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + ReportType string `json:"reportType"` + OriginalRequestClientOrderID string `json:"originalRequestClientOrderId"` + TradeQuantity float64 `json:"tradeQuantity,string"` + TradePrice float64 `json:"tradePrice,string"` + TradeID float64 `json:"tradeId"` + TradeFee float64 `json:"tradeFee,string"` } // WsReportResponse report response for auth subscription to reports diff --git a/exchanges/hitbtc/hitbtc_websocket.go b/exchanges/hitbtc/hitbtc_websocket.go index 5675e0e6..a5867de1 100644 --- a/exchanges/hitbtc/hitbtc_websocket.go +++ b/exchanges/hitbtc/hitbtc_websocket.go @@ -15,6 +15,7 @@ import ( exchange "github.com/thrasher-corp/gocryptotrader/exchanges" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/nonce" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" @@ -26,6 +27,7 @@ const ( hitbtcWebsocketAddress = "wss://api.hitbtc.com/api/2/ws" rpcVersion = "2.0" rateLimit = 20 + errAuthFailed = 1002 ) var requestID nonce.Nonce @@ -40,7 +42,7 @@ func (h *HitBTC) WsConnect() error { if err != nil { return err } - go h.WsHandleData() + go h.wsReadData() err = h.wsLogin() if err != nil { log.Errorf(log.ExchangeSys, "%v - authentication failed: %v\n", h.Name, err) @@ -51,8 +53,8 @@ func (h *HitBTC) WsConnect() error { return nil } -// WsHandleData handles websocket data -func (h *HitBTC) WsHandleData() { +// wsReadData receives and passes on websocket messages for processing +func (h *HitBTC) wsReadData() { h.Websocket.Wg.Add(1) defer func() { @@ -63,7 +65,6 @@ func (h *HitBTC) WsHandleData() { select { case <-h.Websocket.ShutdownC: return - default: resp, err := h.WebsocketConn.ReadMessage() if err != nil { @@ -72,50 +73,82 @@ func (h *HitBTC) WsHandleData() { } h.Websocket.TrafficAlert <- struct{}{} - var init capture - err = json.Unmarshal(resp.Raw, &init) + err = h.wsHandleData(resp.Raw) if err != nil { h.Websocket.DataHandler <- err - continue - } - if init.Error.Code == 1002 { - h.Websocket.SetCanUseAuthenticatedEndpoints(false) - } - if init.ID > 0 { - h.WebsocketConn.AddResponseWithID(init.ID, resp.Raw) - continue - } - if init.Error.Message != "" || init.Error.Code != 0 { - h.Websocket.DataHandler <- fmt.Errorf("hitbtc.go error - Code: %d, Message: %s", - init.Error.Code, - init.Error.Message) - continue - } - if _, ok := init.Result.(bool); ok { - continue - } - if init.Method != "" { - h.handleSubscriptionUpdates(resp, init) - } else { - h.handleCommandResponses(resp, init) } } } } -func (h *HitBTC) handleSubscriptionUpdates(resp wshandler.WebsocketResponse, init capture) { - switch init.Method { +func (h *HitBTC) wsGetTableName(respRaw []byte) (string, error) { + var init capture + err := json.Unmarshal(respRaw, &init) + if err != nil { + return "", err + } + if init.Error.Code == errAuthFailed { + h.Websocket.SetCanUseAuthenticatedEndpoints(false) + } + if init.ID > 0 { + if h.WebsocketConn.IsIDWaitingForResponse(init.ID) { + h.WebsocketConn.SetResponseIDAndData(init.ID, respRaw) + return "", nil + } + } + if init.Error.Message != "" || init.Error.Code != 0 { + return "", fmt.Errorf("hitbtc.go error - Code: %d, Message: %s", + init.Error.Code, + init.Error.Message) + } + if _, ok := init.Result.(bool); ok { + return "", nil + } + if init.Method != "" { + return init.Method, nil + } + switch resultType := init.Result.(type) { + case map[string]interface{}: + if reportType, ok := resultType["reportType"].(string); ok { + return reportType, nil + } + // check for ids - means it was a specific request + // and can't go through normal processing + if responseID, ok := resultType["id"].(string); ok { + if responseID != "" { + return "", nil + } + } + case []interface{}: + if len(resultType) == 0 { + h.Websocket.DataHandler <- fmt.Sprintf("No data returned. ID: %v", init.ID) + return "", nil + } + + data := resultType[0].(map[string]interface{}) + if _, ok := data["clientOrderId"]; ok { + return "order", nil + } else if _, ok := data["available"]; ok { + return "trading", nil + } + } + h.Websocket.DataHandler <- wshandler.UnhandledMessageWarning{Message: h.Name + wshandler.UnhandledMessage + string(respRaw)} + return "", nil +} + +func (h *HitBTC) wsHandleData(respRaw []byte) error { + name, err := h.wsGetTableName(respRaw) + if err != nil { + return err + } + switch name { + case "": + return nil case "ticker": var wsTicker WsTicker - err := json.Unmarshal(resp.Raw, &wsTicker) + err := json.Unmarshal(respRaw, &wsTicker) if err != nil { - h.Websocket.DataHandler <- err - return - } - ts, err := time.Parse(time.RFC3339, wsTicker.Params.Timestamp) - if err != nil { - h.Websocket.DataHandler <- err - return + return err } h.Websocket.DataHandler <- &ticker.Price{ ExchangeName: h.Name, @@ -127,105 +160,102 @@ func (h *HitBTC) handleSubscriptionUpdates(resp wshandler.WebsocketResponse, ini Bid: wsTicker.Params.Bid, Ask: wsTicker.Params.Ask, Last: wsTicker.Params.Last, - LastUpdated: ts, + LastUpdated: wsTicker.Params.Timestamp, AssetType: asset.Spot, Pair: currency.NewPairFromFormattedPairs(wsTicker.Params.Symbol, h.GetEnabledPairs(asset.Spot), h.GetPairFormat(asset.Spot, true)), } case "snapshotOrderbook": var obSnapshot WsOrderbook - err := json.Unmarshal(resp.Raw, &obSnapshot) + err := json.Unmarshal(respRaw, &obSnapshot) if err != nil { - h.Websocket.DataHandler <- err + return err } err = h.WsProcessOrderbookSnapshot(obSnapshot) if err != nil { - h.Websocket.DataHandler <- err + return err } case "updateOrderbook": var obUpdate WsOrderbook - err := json.Unmarshal(resp.Raw, &obUpdate) + err := json.Unmarshal(respRaw, &obUpdate) if err != nil { - h.Websocket.DataHandler <- err + return err + } + err = h.WsProcessOrderbookUpdate(obUpdate) + if err != nil { + return err } - h.WsProcessOrderbookUpdate(obUpdate) case "snapshotTrades": var tradeSnapshot WsTrade - err := json.Unmarshal(resp.Raw, &tradeSnapshot) + err := json.Unmarshal(respRaw, &tradeSnapshot) if err != nil { - h.Websocket.DataHandler <- err + return err } case "updateTrades": var tradeUpdates WsTrade - err := json.Unmarshal(resp.Raw, &tradeUpdates) + err := json.Unmarshal(respRaw, &tradeUpdates) if err != nil { - h.Websocket.DataHandler <- err + return err } case "activeOrders": - var activeOrders WsActiveOrdersResponse - err := json.Unmarshal(resp.Raw, &activeOrders) + var o wsActiveOrdersResponse + err := json.Unmarshal(respRaw, &o) if err != nil { - h.Websocket.DataHandler <- err + return err } - h.Websocket.DataHandler <- activeOrders + for i := range o.Params { + err = h.wsHandleOrderData(&o.Params[i]) + if err != nil { + return err + } + } + case "trading": + var trades WsGetTradingBalanceResponse + err := json.Unmarshal(respRaw, &trades) + if err != nil { + return err + } + h.Websocket.DataHandler <- trades case "report": - var reportData WsReportResponse - err := json.Unmarshal(resp.Raw, &reportData) + var o wsReportResponse + err := json.Unmarshal(respRaw, &o) if err != nil { - h.Websocket.DataHandler <- err - } - h.Websocket.DataHandler <- reportData - } -} - -func (h *HitBTC) handleCommandResponses(resp wshandler.WebsocketResponse, init capture) { - switch resultType := init.Result.(type) { - case map[string]interface{}: - switch resultType["reportType"].(string) { - case "new": - var response WsSubmitOrderSuccessResponse - err := json.Unmarshal(resp.Raw, &response) - if err != nil { - h.Websocket.DataHandler <- err - } - h.Websocket.DataHandler <- response - case "canceled": - var response WsCancelOrderResponse - err := json.Unmarshal(resp.Raw, &response) - if err != nil { - h.Websocket.DataHandler <- err - } - h.Websocket.DataHandler <- response - case "replaced": - var response WsReplaceOrderResponse - err := json.Unmarshal(resp.Raw, &response) - if err != nil { - h.Websocket.DataHandler <- err - } - h.Websocket.DataHandler <- response - } - case []interface{}: - if len(resultType) == 0 { - h.Websocket.DataHandler <- fmt.Sprintf("No data returned. ID: %v", init.ID) - return - } - data := resultType[0].(map[string]interface{}) - if _, ok := data["clientOrderId"]; ok { - var response WsActiveOrdersResponse - err := json.Unmarshal(resp.Raw, &response) - if err != nil { - h.Websocket.DataHandler <- err - } - h.Websocket.DataHandler <- response - } else if _, ok := data["available"]; ok { - var response WsGetTradingBalanceResponse - err := json.Unmarshal(resp.Raw, &response) - if err != nil { - h.Websocket.DataHandler <- err - } - h.Websocket.DataHandler <- response + return err } + err = h.wsHandleOrderData(&o.OrderData) + if err != nil { + return err + } + case "order": + var o wsActiveOrderRequestResponse + err := json.Unmarshal(respRaw, &o) + if err != nil { + return err + } + for i := range o.OrderData { + err = h.wsHandleOrderData(&o.OrderData[i]) + if err != nil { + return err + } + } + case + "replaced", + "canceled", + "new": + var o wsOrderResponse + err := json.Unmarshal(respRaw, &o) + if err != nil { + return err + } + err = h.wsHandleOrderData(&o.OrderData) + if err != nil { + return err + } + default: + h.Websocket.DataHandler <- wshandler.UnhandledMessageWarning{Message: h.Name + wshandler.UnhandledMessage + string(respRaw)} + return nil } + return nil } // WsProcessOrderbookSnapshot processes a full orderbook snapshot to a local cache @@ -269,6 +299,68 @@ func (h *HitBTC) WsProcessOrderbookSnapshot(ob WsOrderbook) error { return nil } +func (h *HitBTC) wsHandleOrderData(o *wsOrderData) error { + var trades []order.TradeHistory + if o.TradeID > 0 { + trades = append(trades, order.TradeHistory{ + Price: o.TradePrice, + Amount: o.TradeQuantity, + Fee: o.TradeFee, + Exchange: h.Name, + TID: strconv.FormatFloat(o.TradeID, 'f', -1, 64), + Timestamp: o.UpdatedAt, + }) + } + oType, err := order.StringToOrderType(o.Type) + if err != nil { + h.Websocket.DataHandler <- order.ClassificationError{ + Exchange: h.Name, + OrderID: o.ID, + Err: err, + } + } + o.Status = strings.Replace(o.Status, "canceled", "cancelled", 1) + oStatus, err := order.StringToOrderStatus(o.Status) + if err != nil { + h.Websocket.DataHandler <- order.ClassificationError{ + Exchange: h.Name, + OrderID: o.ID, + Err: err, + } + } + oSide, err := order.StringToOrderSide(o.Side) + if err != nil { + h.Websocket.DataHandler <- order.ClassificationError{ + Exchange: h.Name, + OrderID: o.ID, + Err: err, + } + } + p := currency.NewPairFromString(o.Symbol) + var a asset.Item + a, err = h.GetPairAssetType(p) + if err != nil { + return err + } + h.Websocket.DataHandler <- &order.Detail{ + Price: o.Price, + Amount: o.Quantity, + ExecutedAmount: o.CumQuantity, + RemainingAmount: o.Quantity - o.CumQuantity, + Exchange: h.Name, + ID: o.ID, + Type: oType, + Side: oSide, + Status: oStatus, + AssetType: a, + Date: o.CreatedAt, + LastUpdated: o.UpdatedAt, + Pair: p, + Trades: trades, + } + return nil +} + // WsProcessOrderbookUpdate updates a local cache func (h *HitBTC) WsProcessOrderbookUpdate(update WsOrderbook) error { if len(update.Params.Bid) == 0 && len(update.Params.Ask) == 0 { @@ -397,14 +489,14 @@ func (h *HitBTC) wsLogin() error { return fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", h.Name) } h.Websocket.SetCanUseAuthenticatedEndpoints(true) - nonce := strconv.FormatInt(time.Now().Unix(), 10) - hmac := crypto.GetHMAC(crypto.HashSHA256, []byte(nonce), []byte(h.API.Credentials.Secret)) + n := strconv.FormatInt(time.Now().Unix(), 10) + hmac := crypto.GetHMAC(crypto.HashSHA256, []byte(n), []byte(h.API.Credentials.Secret)) request := WsLoginRequest{ Method: "login", Params: WsLoginData{ Algo: "HS256", PKey: h.API.Credentials.Key, - Nonce: nonce, + Nonce: n, Signature: crypto.HexEncodeToString(hmac), }, } @@ -507,7 +599,7 @@ func (h *HitBTC) wsReplaceOrder(clientOrderID string, quantity, price float64) ( } // wsGetActiveOrders sends a websocket message to get all active orders -func (h *HitBTC) wsGetActiveOrders() (*WsActiveOrdersResponse, error) { +func (h *HitBTC) wsGetActiveOrders() (*wsActiveOrdersResponse, error) { if !h.Websocket.CanUseAuthenticatedEndpoints() { return nil, fmt.Errorf("%v not authenticated, cannot place order", h.Name) } @@ -520,7 +612,7 @@ func (h *HitBTC) wsGetActiveOrders() (*WsActiveOrdersResponse, error) { if err != nil { return nil, fmt.Errorf("%v %v", h.Name, err) } - var response WsActiveOrdersResponse + var response wsActiveOrdersResponse err = json.Unmarshal(resp, &response) if err != nil { return nil, fmt.Errorf("%v %v", h.Name, err) diff --git a/exchanges/hitbtc/hitbtc_wrapper.go b/exchanges/hitbtc/hitbtc_wrapper.go index 254e8fe3..9b2ee98d 100644 --- a/exchanges/hitbtc/hitbtc_wrapper.go +++ b/exchanges/hitbtc/hitbtc_wrapper.go @@ -102,6 +102,8 @@ func (h *HitBTC) SetDefaults() { SubmitOrder: true, CancelOrder: true, MessageSequenceNumbers: true, + GetOrders: true, + GetOrder: true, }, WithdrawPermissions: exchange.AutoWithdrawCrypto | exchange.NoFiatWithdrawals, @@ -396,7 +398,7 @@ func (h *HitBTC) SubmitOrder(o *order.Submit) (order.SubmitResponse, error) { } if h.Websocket.IsConnected() && h.Websocket.CanUseAuthenticatedEndpoints() { var response *WsSubmitOrderSuccessResponse - response, err = h.wsPlaceOrder(o.Pair, o.OrderSide.String(), o.Amount, o.Price) + response, err = h.wsPlaceOrder(o.Pair, o.Side.String(), o.Amount, o.Price) if err != nil { return submitOrderResponse, err } @@ -409,15 +411,15 @@ func (h *HitBTC) SubmitOrder(o *order.Submit) (order.SubmitResponse, error) { response, err = h.PlaceOrder(o.Pair.String(), o.Price, o.Amount, - strings.ToLower(o.OrderType.String()), - strings.ToLower(o.OrderSide.String())) + strings.ToLower(o.Type.String()), + strings.ToLower(o.Side.String())) if err != nil { return submitOrderResponse, err } if response.OrderNumber > 0 { submitOrderResponse.OrderID = strconv.FormatInt(response.OrderNumber, 10) } - if o.OrderType == order.Market { + if o.Type == order.Market { submitOrderResponse.FullyMatched = true } } @@ -434,7 +436,7 @@ func (h *HitBTC) ModifyOrder(action *order.Modify) (string, error) { // CancelOrder cancels an order by its corresponding ID number func (h *HitBTC) CancelOrder(order *order.Cancel) error { - orderIDInt, err := strconv.ParseInt(order.OrderID, 10, 64) + orderIDInt, err := strconv.ParseInt(order.ID, 10, 64) if err != nil { return err } @@ -522,13 +524,13 @@ func (h *HitBTC) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) // GetActiveOrders retrieves any orders that are active/open func (h *HitBTC) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, error) { - if len(req.Currencies) == 0 { + if len(req.Pairs) == 0 { return nil, errors.New("currency must be supplied") } var allOrders []OrderHistoryResponse - for i := range req.Currencies { - resp, err := h.GetOpenOrders(req.Currencies[i].String()) + for i := range req.Pairs { + resp, err := h.GetOpenOrders(req.Pairs[i].String()) if err != nil { return nil, err } @@ -541,31 +543,31 @@ func (h *HitBTC) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, e h.GetPairFormat(asset.Spot, false).Delimiter) side := order.Side(strings.ToUpper(allOrders[i].Side)) orders = append(orders, order.Detail{ - ID: allOrders[i].ID, - Amount: allOrders[i].Quantity, - Exchange: h.Name, - Price: allOrders[i].Price, - OrderDate: allOrders[i].CreatedAt, - OrderSide: side, - CurrencyPair: symbol, + ID: allOrders[i].ID, + Amount: allOrders[i].Quantity, + Exchange: h.Name, + Price: allOrders[i].Price, + Date: allOrders[i].CreatedAt, + Side: side, + Pair: symbol, }) } order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) - order.FilterOrdersBySide(&orders, req.OrderSide) + order.FilterOrdersBySide(&orders, req.Side) return orders, nil } // GetOrderHistory retrieves account order information // Can Limit response to specific order status func (h *HitBTC) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail, error) { - if len(req.Currencies) == 0 { + if len(req.Pairs) == 0 { return nil, errors.New("currency must be supplied") } var allOrders []OrderHistoryResponse - for i := range req.Currencies { - resp, err := h.GetOrders(req.Currencies[i].String()) + for i := range req.Pairs { + resp, err := h.GetOrders(req.Pairs[i].String()) if err != nil { return nil, err } @@ -578,18 +580,18 @@ func (h *HitBTC) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail, e h.GetPairFormat(asset.Spot, false).Delimiter) side := order.Side(strings.ToUpper(allOrders[i].Side)) orders = append(orders, order.Detail{ - ID: allOrders[i].ID, - Amount: allOrders[i].Quantity, - Exchange: h.Name, - Price: allOrders[i].Price, - OrderDate: allOrders[i].CreatedAt, - OrderSide: side, - CurrencyPair: symbol, + ID: allOrders[i].ID, + Amount: allOrders[i].Quantity, + Exchange: h.Name, + Price: allOrders[i].Price, + Date: allOrders[i].CreatedAt, + Side: side, + Pair: symbol, }) } order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) - order.FilterOrdersBySide(&orders, req.OrderSide) + order.FilterOrdersBySide(&orders, req.Side) return orders, nil } diff --git a/exchanges/huobi/huobi_test.go b/exchanges/huobi/huobi_test.go index 64614da9..7d7377b0 100644 --- a/exchanges/huobi/huobi_test.go +++ b/exchanges/huobi/huobi_test.go @@ -49,7 +49,8 @@ func TestMain(m *testing.M) { if err != nil { log.Fatal("Huobi setup error", err) } - + h.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride() + h.Websocket.TrafficAlert = sharedtestvalues.GetWebsocketStructChannelOverride() os.Exit(m.Run()) } @@ -63,7 +64,7 @@ func setupWsTests(t *testing.T) { comms = make(chan WsMessage, sharedtestvalues.WebsocketChannelOverrideCapacity) h.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride() h.Websocket.TrafficAlert = sharedtestvalues.GetWebsocketStructChannelOverride() - go h.WsHandleData() + go h.wsReadData() h.AuthenticatedWebsocketConn = &wshandler.WebsocketConnection{ ExchangeName: h.Name, URL: wsAccountsOrdersURL, @@ -419,8 +420,8 @@ func TestFormatWithdrawPermissions(t *testing.T) { func TestGetActiveOrders(t *testing.T) { var getOrdersRequest = order.GetOrdersRequest{ - OrderType: order.AnyType, - Currencies: []currency.Pair{currency.NewPair(currency.BTC, currency.USDT)}, + Type: order.AnyType, + Pairs: []currency.Pair{currency.NewPair(currency.BTC, currency.USDT)}, } _, err := h.GetActiveOrders(&getOrdersRequest) @@ -433,8 +434,8 @@ func TestGetActiveOrders(t *testing.T) { func TestGetOrderHistory(t *testing.T) { var getOrdersRequest = order.GetOrdersRequest{ - OrderType: order.AnyType, - Currencies: []currency.Pair{currency.NewPair(currency.BTC, currency.USDT)}, + Type: order.AnyType, + Pairs: []currency.Pair{currency.NewPair(currency.BTC, currency.USDT)}, } _, err := h.GetOrderHistory(&getOrdersRequest) @@ -470,11 +471,11 @@ func TestSubmitOrder(t *testing.T) { Base: currency.BTC, Quote: currency.USDT, }, - OrderSide: order.Buy, - OrderType: order.Limit, - Price: 1, - Amount: 1, - ClientID: strconv.FormatInt(accounts[0].ID, 10), + Side: order.Buy, + Type: order.Limit, + Price: 1, + Amount: 1, + ClientID: strconv.FormatInt(accounts[0].ID, 10), } response, err := h.SubmitOrder(orderSubmission) if areTestAPIKeysSet() && (err != nil || !response.IsOrderPlaced) { @@ -489,10 +490,10 @@ func TestCancelExchangeOrder(t *testing.T) { currencyPair := currency.NewPair(currency.LTC, currency.BTC) var orderCancellation = &order.Cancel{ - OrderID: "1", + ID: "1", WalletAddress: core.BitcoinDonationAddress, AccountID: "1", - CurrencyPair: currencyPair, + Pair: currencyPair, } err := h.CancelOrder(orderCancellation) @@ -512,10 +513,10 @@ func TestCancelAllExchangeOrders(t *testing.T) { currencyPair := currency.NewPair(currency.LTC, currency.BTC) var orderCancellation = order.Cancel{ - OrderID: "1", + ID: "1", WalletAddress: core.BitcoinDonationAddress, AccountID: "1", - CurrencyPair: currencyPair, + Pair: currencyPair, } resp, err := h.CancelAllOrders(&orderCancellation) @@ -655,7 +656,379 @@ func TestWsGetOrderDetails(t *testing.T) { if err != nil { t.Fatal(err) } - if resp.ErrorCode > 0 && (orderID == "123" && resp.ErrorCode != 10022) { + if resp.ErrorCode > 0 && resp.ErrorCode != 10022 { t.Error(resp.ErrorMessage) } } + +func TestWsSubResponse(t *testing.T) { + pressXToJSON := []byte(`{ + "op": "sub", + "cid": "123", + "err-code": 0, + "ts": 1489474081631, + "topic": "accounts" +}`) + err := h.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsKline(t *testing.T) { + pressXToJSON := []byte(`{ + "ch": "market.btcusdt.kline.1min", + "ts": 1489474082831, + "tick": { + "id": 1489464480, + "amount": 0.0, + "count": 0, + "open": 7962.62, + "close": 7962.62, + "low": 7962.62, + "high": 7962.62, + "vol": 0.0 + } +}`) + err := h.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsUnsubscribe(t *testing.T) { + pressXToJSON := []byte(`{ + "id": "id4", + "status": "ok", + "unsubbed": "market.btcusdt.trade.detail", + "ts": 1494326028889 +}`) + err := h.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsKlineArray(t *testing.T) { + pressXToJSON := []byte(`{ + "status": "ok", + "rep": "market.btcusdt.kline.1min", + "data": [ + { + "amount": 1.6206, + "count": 3, + "id": 1494465840, + "open": 9887.00, + "close": 9885.00, + "low": 9885.00, + "high": 9887.00, + "vol": 16021.632026 + }, + { + "amount": 2.2124, + "count": 6, + "id": 1494465900, + "open": 9885.00, + "close": 9880.00, + "low": 9880.00, + "high": 9885.00, + "vol": 21859.023500 + } + ] +}`) + err := h.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsMarketDepth(t *testing.T) { + pressXToJSON := []byte(`{ + "ch": "market.htusdt.depth.step0", + "ts": 1572362902027, + "tick": { + "bids": [ + [3.7721, 344.86], + [3.7709, 46.66] + ], + "asks": [ + [3.7745, 15.44], + [3.7746, 70.52] + ], + "version": 100434317651, + "ts": 1572362902012 + } +}`) + err := h.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsBestBidOffer(t *testing.T) { + pressXToJSON := []byte(`{ + "ch": "market.btcusdt.bbo", + "ts": 1489474082831, + "tick": { + "symbol": "btcusdt", + "quoteTime": "1489474082811", + "bid": "10008.31", + "bidSize": "0.01", + "ask": "10009.54", + "askSize": "0.3" + } + }`) + err := h.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsTradeDetail(t *testing.T) { + pressXToJSON := []byte(`{ + "ch": "market.btcusdt.trade.detail", + "ts": 1489474082831, + "tick": { + "id": 14650745135, + "ts": 1533265950234, + "data": [ + { + "amount": 0.0099, + "ts": 1533265950234, + "id": 146507451359183894799, + "tradeId": 102043495674, + "price": 401.74, + "direction": "buy" + } + ] + } + }`) + err := h.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsTicker(t *testing.T) { + pressXToJSON := []byte(`{ + "rep": "market.btcusdt.detail", + "id": "id11", + "data":{ + "amount": 12224.2922, + "open": 9790.52, + "close": 10195.00, + "high": 10300.00, + "ts": 1494496390000, + "id": 1494496390, + "count": 15195, + "low": 9657.00, + "vol": 121906001.754751 + } +}`) + err := h.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsAccountUpdate(t *testing.T) { + pressXToJSON := []byte(`{ + "op": "notify", + "ts": 1522856623232, + "topic": "accounts", + "data": { + "event": "order.place", + "list": [ + { + "account-id": 419013, + "currency": "usdt", + "type": "trade", + "balance": "500009195917.4362872650" + } + ] + } + }`) + err := h.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsOrderUpdate(t *testing.T) { + pressXToJSON := []byte(`{ + "op": "notify", + "topic": "orders.htusdt", + "ts": 1522856623232, + "data": { + "seq-id": 94984, + "order-id": 2039498445, + "symbol": "btcusdt", + "account-id": 100077, + "order-amount": "5000.000000000000000000", + "order-price": "1.662100000000000000", + "created-at": 1522858623622, + "order-type": "buy-limit", + "order-source": "api", + "order-state": "filled", + "role": "taker", + "price": "1.662100000000000000", + "filled-amount": "5000.000000000000000000", + "unfilled-amount": "0.000000000000000000", + "filled-cash-amount": "8301.357280000000000000", + "filled-fees": "8.000000000000000000" + } +}`) + err := h.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsSubsbOp(t *testing.T) { + pressXToJSON := []byte(`{ + "op": "unsub", + "topic": "accounts", + "cid": "123" + }`) + err := h.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } + pressXToJSON = []byte(`{ + "op": "sub", + "cid": "123", + "err-code": 0, + "ts": 1489474081631, + "topic": "accounts" + }`) + err = h.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsMarketByPrice(t *testing.T) { + pressXToJSON := []byte(`{ + "ch": "market.btcusdt.mbp.150", + "ts": 1573199608679, + "tick": { + "seqNum": 100020146795, + "prevSeqNum": 100020146794, + "bids": [], + "asks": [ + [645.140000000000000000, 26.755973959140651643] + ] + } + }`) + err := h.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } + pressXToJSON = []byte(`{ + "id": "id2", + "rep": "market.btcusdt.mbp.150", + "status": "ok", + "data": { + "seqNum": 100020142010, + "bids": [ + [618.37, 71.594], + [423.33, 77.726], + [223.18, 47.997], + [219.34, 24.82], + [210.34, 94.463] + ], + "asks": [ + [650.59, 14.909733438479636], + [650.63, 97.996], + [650.77, 97.465], + [651.23, 83.973], + [651.42, 34.465] + ] + } + }`) + err = h.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsOrdersUpdate(t *testing.T) { + pressXToJSON := []byte(`{ + "op": "notify", + "ts": 1522856623232, + "topic": "orders.btcusdt.update", + "data": { + "unfilled-amount": "0.000000000000000000", + "filled-amount": "5000.000000000000000000", + "price": "1.662100000000000000", + "order-id": 2039498445, + "symbol": "btcusdt", + "match-id": 94984, + "filled-cash-amount": "8301.357280000000000000", + "role": "taker|maker", + "order-state": "filled", + "client-order-id": "a0001", + "order-type": "buy-limit" + } + }`) + err := h.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestStringToOrderStatus(t *testing.T) { + type TestCases struct { + Case string + Result order.Status + } + testCases := []TestCases{ + {Case: "submitted", Result: order.New}, + {Case: "canceled", Result: order.Cancelled}, + {Case: "partial-filled", Result: order.PartiallyFilled}, + {Case: "partial-canceled", Result: order.PartiallyCancelled}, + {Case: "LOL", Result: order.UnknownStatus}, + } + for i := range testCases { + result, _ := stringToOrderStatus(testCases[i].Case) + if result != testCases[i].Result { + t.Errorf("Exepcted: %v, received: %v", testCases[i].Result, result) + } + } +} + +func TestStringToOrderSide(t *testing.T) { + type TestCases struct { + Case string + Result order.Side + } + testCases := []TestCases{ + {Case: "buy-limit", Result: order.Buy}, + {Case: "sell-limit", Result: order.Sell}, + {Case: "woah-nelly", Result: order.UnknownSide}, + } + for i := range testCases { + result, _ := stringToOrderSide(testCases[i].Case) + if result != testCases[i].Result { + t.Errorf("Exepcted: %v, received: %v", testCases[i].Result, result) + } + } +} + +func TestStringToOrderType(t *testing.T) { + type TestCases struct { + Case string + Result order.Type + } + testCases := []TestCases{ + {Case: "buy-limit", Result: order.Limit}, + {Case: "sell-market", Result: order.Market}, + {Case: "woah-nelly", Result: order.UnknownType}, + } + for i := range testCases { + result, _ := stringToOrderType(testCases[i].Case) + if result != testCases[i].Result { + t.Errorf("Exepcted: %v, received: %v", testCases[i].Result, result) + } + } +} diff --git a/exchanges/huobi/huobi_types.go b/exchanges/huobi/huobi_types.go index acd7d0a2..1ced0b7f 100644 --- a/exchanges/huobi/huobi_types.go +++ b/exchanges/huobi/huobi_types.go @@ -331,14 +331,18 @@ type WsRequest struct { // WsResponse defines a response from the websocket connection when there // is an error type WsResponse struct { - TS int64 `json:"ts"` - Status string `json:"status"` - ErrorCode interface{} `json:"err-code"` - ErrorMessage string `json:"err-msg"` - Ping int64 `json:"ping"` - Channel string `json:"ch"` - Subscribed string `json:"subbed"` - ClientID int64 `json:"cid,string,omitempty"` + Op string `json:"op"` + TS int64 `json:"ts"` + Status string `json:"status"` + ErrorCode int64 `json:"err-code"` + ErrorMessage string `json:"err-msg"` + Ping int64 `json:"ping"` + Channel string `json:"ch"` + Rep string `json:"rep"` + Topic string `json:"topic"` + Subscribed string `json:"subbed"` + UnSubscribed string `json:"unsubbed"` + ClientID int64 `json:"cid,string"` } // WsHeartBeat defines a heartbeat request @@ -377,6 +381,7 @@ type WsKline struct { // WsTick stores websocket ticker data type WsTick struct { Channel string `json:"ch"` + Rep string `json:"rep"` Timestamp int64 `json:"ts"` Tick struct { Amount float64 `json:"amount"` @@ -478,20 +483,9 @@ type WsAuthenticatedOrdersListRequest struct { ClientID int64 `json:"cid,string,omitempty"` } -// WsAuthenticatedDataResponse response from authenticated connection -type WsAuthenticatedDataResponse struct { - Op string `json:"op,omitempty"` - Ts int64 `json:"ts,omitempty"` - Topic string `json:"topic,omitempty"` - ErrorCode int64 `json:"err-code,omitempty"` - ErrorMessage string `json:"err-msg,omitempty"` - Ping int64 `json:"ping,omitempty"` - ClientID int64 `json:"cid,string,omitempty"` -} - // WsAuthenticatedAccountsResponse response from Accounts authenticated subscription type WsAuthenticatedAccountsResponse struct { - WsAuthenticatedDataResponse + WsResponse Data WsAuthenticatedAccountsResponseData `json:"data"` } @@ -511,11 +505,11 @@ type WsAuthenticatedAccountsResponseDataList struct { // WsAuthenticatedOrdersUpdateResponse response from OrdersUpdate authenticated subscription type WsAuthenticatedOrdersUpdateResponse struct { - WsAuthenticatedDataResponse + WsResponse Data WsAuthenticatedOrdersUpdateResponseData `json:"data"` } -// WsAuthenticatedOrdersUpdateResponseData order updatedata +// WsAuthenticatedOrdersUpdateResponseData order update data type WsAuthenticatedOrdersUpdateResponseData struct { UnfilledAmount float64 `json:"unfilled-amount,string"` FilledAmount float64 `json:"filled-amount,string"` @@ -526,14 +520,21 @@ type WsAuthenticatedOrdersUpdateResponseData struct { FilledCashAmount float64 `json:"filled-cash-amount,string"` Role string `json:"role"` OrderState string `json:"order-state"` + OrderType string `json:"order-type"` } // WsAuthenticatedOrdersResponse response from Orders authenticated subscription type WsAuthenticatedOrdersResponse struct { - WsAuthenticatedDataResponse + WsResponse Data []WsAuthenticatedOrdersResponseData `json:"data"` } +// WsOldOrderUpdate response from Orders authenticated subscription +type WsOldOrderUpdate struct { + WsResponse + Data WsAuthenticatedOrdersResponseData `json:"data"` +} + // WsAuthenticatedOrdersResponseData order data type WsAuthenticatedOrdersResponseData struct { SeqID int64 `json:"seq-id"` @@ -556,7 +557,7 @@ type WsAuthenticatedOrdersResponseData struct { // WsAuthenticatedAccountsListResponse response from AccountsList authenticated endpoint type WsAuthenticatedAccountsListResponse struct { - WsAuthenticatedDataResponse + WsResponse Data []WsAuthenticatedAccountsListResponseData `json:"data"` } @@ -577,13 +578,13 @@ type WsAuthenticatedAccountsListResponseDataList struct { // WsAuthenticatedOrdersListResponse response from OrdersList authenticated endpoint type WsAuthenticatedOrdersListResponse struct { - WsAuthenticatedDataResponse + WsResponse Data []OrderInfo `json:"data"` } // WsAuthenticatedOrderDetailResponse response from OrderDetail authenticated endpoint type WsAuthenticatedOrderDetailResponse struct { - WsAuthenticatedDataResponse + WsResponse Data OrderInfo `json:"data"` } @@ -591,3 +592,18 @@ type WsAuthenticatedOrderDetailResponse struct { type WsPong struct { Pong int64 `json:"pong"` } + +type wsKLineResponseThing struct { + Data []struct { + Amount float64 `json:"amount"` + Close float64 `json:"close"` + Count float64 `json:"count"` + High float64 `json:"high"` + ID int64 `json:"id"` + Low float64 `json:"low"` + Open float64 `json:"open"` + Volume float64 `json:"vol"` + } `json:"data"` + Rep string `json:"rep"` + Status string `json:"status"` +} diff --git a/exchanges/huobi/huobi_websocket.go b/exchanges/huobi/huobi_websocket.go index 307bd34f..cfa6724a 100644 --- a/exchanges/huobi/huobi_websocket.go +++ b/exchanges/huobi/huobi_websocket.go @@ -6,6 +6,7 @@ import ( "fmt" "net/http" "net/url" + "strconv" "strings" "time" @@ -14,6 +15,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" @@ -72,7 +74,7 @@ func (h *HUOBI) WsConnect() error { h.Websocket.SetCanUseAuthenticatedEndpoints(false) } - go h.WsHandleData() + go h.wsReadData() h.GenerateDefaultSubscriptions() return nil @@ -83,7 +85,7 @@ func (h *HUOBI) wsDial(dialer *websocket.Dialer) error { if err != nil { return err } - go h.wsMultiConnectionFunnel(h.WebsocketConn, wsMarketURL) + go h.wsFunnelConnectionData(h.WebsocketConn, wsMarketURL) return nil } @@ -95,12 +97,12 @@ func (h *HUOBI) wsAuthenticatedDial(dialer *websocket.Dialer) error { if err != nil { return err } - go h.wsMultiConnectionFunnel(h.AuthenticatedWebsocketConn, wsAccountsOrdersURL) + go h.wsFunnelConnectionData(h.AuthenticatedWebsocketConn, wsAccountsOrdersURL) return nil } -// wsMultiConnectionFunnel manages data from multiple endpoints and passes it to a channel -func (h *HUOBI) wsMultiConnectionFunnel(ws *wshandler.WebsocketConnection, url string) { +// wsFunnelConnectionData manages data from multiple endpoints and passes it to a channel +func (h *HUOBI) wsFunnelConnectionData(ws *wshandler.WebsocketConnection, url string) { h.Websocket.Wg.Add(1) defer h.Websocket.Wg.Done() for { @@ -119,8 +121,8 @@ func (h *HUOBI) wsMultiConnectionFunnel(ws *wshandler.WebsocketConnection, url s } } -// WsHandleData handles data read from the websocket connection -func (h *HUOBI) WsHandleData() { +// wsReadData receives and passes on websocket messages for processing +func (h *HUOBI) wsReadData() { h.Websocket.Wg.Add(1) defer h.Websocket.Wg.Done() for { @@ -128,120 +130,201 @@ func (h *HUOBI) WsHandleData() { case <-h.Websocket.ShutdownC: return case resp := <-comms: - switch resp.URL { - case wsMarketURL: - h.wsHandleMarketData(resp) - case wsAccountsOrdersURL: - h.wsHandleAuthenticatedData(resp) + err := h.wsHandleData(resp.Raw) + if err != nil { + h.Websocket.DataHandler <- err } } } } -func (h *HUOBI) wsHandleAuthenticatedData(resp WsMessage) { - var init WsAuthenticatedDataResponse - err := json.Unmarshal(resp.Raw, &init) +func stringToOrderStatus(status string) (order.Status, error) { + switch status { + case "submitted": + return order.New, nil + case "canceled": + return order.Cancelled, nil + case "partial-filled": + return order.PartiallyFilled, nil + case "partial-canceled": + return order.PartiallyCancelled, nil + default: + return order.UnknownStatus, errors.New(status + " not recognised as order status") + } +} + +func stringToOrderSide(side string) (order.Side, error) { + switch { + case strings.Contains(side, "buy"): + return order.Buy, nil + case strings.Contains(side, "sell"): + return order.Sell, nil + } + + return order.UnknownSide, errors.New(side + " not recognised as order side") +} + +func stringToOrderType(oType string) (order.Type, error) { + switch { + case strings.Contains(oType, "limit"): + return order.Limit, nil + case strings.Contains(oType, "market"): + return order.Market, nil + } + + return order.UnknownType, errors.New(oType + " not recognised as order type") +} + +func (h *HUOBI) wsHandleData(respRaw []byte) error { + var init WsResponse + err := json.Unmarshal(respRaw, &init) if err != nil { - h.Websocket.DataHandler <- err - return + return err + } + if init.Subscribed != "" || + init.UnSubscribed != "" || + init.Op == "sub" || + init.Op == "unsub" { + // TODO handle subs + return nil } if init.Ping != 0 { h.sendPingResponse(init.Ping) - return + return nil } if init.ErrorMessage == "api-signature-not-valid" { h.Websocket.SetCanUseAuthenticatedEndpoints(false) - } - if init.Op == "sub" { - if h.Verbose { - log.Debugf(log.ExchangeSys, "%v: %v: Successfully subscribed to %v", h.Name, resp.URL, init.Topic) - } - return + return errors.New(h.Name + " - invalid credentials. Authenticated requests disabled") } if init.ClientID > 0 { - h.AuthenticatedWebsocketConn.AddResponseWithID(init.ClientID, resp.Raw) - return + if h.WebsocketConn.IsIDWaitingForResponse(init.ClientID) { + h.WebsocketConn.SetResponseIDAndData(init.ClientID, respRaw) + return nil + } } switch { case strings.EqualFold(init.Op, authOp): h.Websocket.SetCanUseAuthenticatedEndpoints(true) - var response WsAuthenticatedDataResponse - err := json.Unmarshal(resp.Raw, &response) + err := json.Unmarshal(respRaw, &init) if err != nil { - h.Websocket.DataHandler <- err + return err } - h.Websocket.DataHandler <- response + h.Websocket.DataHandler <- init + case strings.EqualFold(init.Topic, "accounts"): var response WsAuthenticatedAccountsResponse - err := json.Unmarshal(resp.Raw, &response) + err := json.Unmarshal(respRaw, &response) if err != nil { - h.Websocket.DataHandler <- err + return err } h.Websocket.DataHandler <- response + case strings.Contains(init.Topic, "orders") && strings.Contains(init.Topic, "update"): var response WsAuthenticatedOrdersUpdateResponse - err := json.Unmarshal(resp.Raw, &response) + err := json.Unmarshal(respRaw, &response) if err != nil { - h.Websocket.DataHandler <- err + return err } - h.Websocket.DataHandler <- response + data := strings.Split(response.Topic, ".") + if len(data) < 2 { + return errors.New(h.Name + " - currency could not be extracted from response") + } + orderID := strconv.FormatInt(response.Data.OrderID, 10) + var oSide order.Side + oSide, err = stringToOrderSide(response.Data.OrderType) + if err != nil { + h.Websocket.DataHandler <- order.ClassificationError{ + Exchange: h.Name, + OrderID: orderID, + Err: err, + } + } + var oType order.Type + oType, err = stringToOrderType(response.Data.OrderType) + if err != nil { + h.Websocket.DataHandler <- order.ClassificationError{ + Exchange: h.Name, + OrderID: orderID, + Err: err, + } + } + var oStatus order.Status + oStatus, err = stringToOrderStatus(response.Data.OrderState) + if err != nil { + h.Websocket.DataHandler <- order.ClassificationError{ + Exchange: h.Name, + OrderID: orderID, + Err: err, + } + } + var p currency.Pair + var a asset.Item + p, a, err = h.GetRequestFormattedPairAndAssetType(data[1]) + if err != nil { + return err + } + h.Websocket.DataHandler <- &order.Detail{ + Price: response.Data.Price, + Amount: response.Data.UnfilledAmount + response.Data.FilledAmount, + ExecutedAmount: response.Data.FilledAmount, + RemainingAmount: response.Data.UnfilledAmount, + Exchange: h.Name, + ID: orderID, + Type: oType, + Side: oSide, + Status: oStatus, + AssetType: a, + LastUpdated: time.Unix(response.TS*1000, 0), + Pair: p, + } + case strings.Contains(init.Topic, "orders"): - var response WsAuthenticatedOrdersResponse - err := json.Unmarshal(resp.Raw, &response) + var response WsOldOrderUpdate + err := json.Unmarshal(respRaw, &response) if err != nil { - h.Websocket.DataHandler <- err + return err } h.Websocket.DataHandler <- response - } -} - -func (h *HUOBI) wsHandleMarketData(resp WsMessage) { - var init WsResponse - err := json.Unmarshal(resp.Raw, &init) - if err != nil { - h.Websocket.DataHandler <- err - return - } - if init.Status == "error" { - h.Websocket.DataHandler <- fmt.Errorf("%v %v Websocket error %s %s", - h.Name, - resp.URL, - init.ErrorCode, - init.ErrorMessage) - return - } - if init.Subscribed != "" { - return - } - if init.Ping != 0 { - h.sendPingResponse(init.Ping) - return - } - - switch { case strings.Contains(init.Channel, "depth"): var depth WsDepth - err := json.Unmarshal(resp.Raw, &depth) + err := json.Unmarshal(respRaw, &depth) if err != nil { - h.Websocket.DataHandler <- err - return + return err } data := strings.Split(depth.Channel, ".") err = h.WsProcessOrderbook(&depth, data[1]) if err != nil { - h.Websocket.DataHandler <- err - return + return err + } + case strings.Contains(init.Rep, "kline"): + var kline wsKLineResponseThing + err := json.Unmarshal(respRaw, &kline) + if err != nil { + return err + } + var curr = strings.Split(init.Rep, ".") + for i := range kline.Data { + h.Websocket.DataHandler <- wshandler.KlineData{ + Timestamp: time.Now(), + Exchange: h.Name, + AssetType: asset.Spot, + Pair: currency.NewPairFromFormattedPairs(curr[1], + h.GetEnabledPairs(asset.Spot), h.GetPairFormat(asset.Spot, true)), + OpenPrice: kline.Data[i].Open, + ClosePrice: kline.Data[i].Close, + HighPrice: kline.Data[i].High, + LowPrice: kline.Data[i].Low, + Volume: kline.Data[i].Volume, + } } - case strings.Contains(init.Channel, "kline"): var kline WsKline - err := json.Unmarshal(resp.Raw, &kline) + err := json.Unmarshal(respRaw, &kline) if err != nil { - h.Websocket.DataHandler <- err - return + return err } data := strings.Split(kline.Channel, ".") h.Websocket.DataHandler <- wshandler.KlineData{ @@ -258,10 +341,9 @@ func (h *HUOBI) wsHandleMarketData(resp WsMessage) { } case strings.Contains(init.Channel, "trade.detail"): var trade WsTrade - err := json.Unmarshal(resp.Raw, &trade) + err := json.Unmarshal(respRaw, &trade) if err != nil { - h.Websocket.DataHandler <- err - return + return err } data := strings.Split(trade.Channel, ".") h.Websocket.DataHandler <- wshandler.TradeData{ @@ -271,14 +353,20 @@ func (h *HUOBI) wsHandleMarketData(resp WsMessage) { h.GetEnabledPairs(asset.Spot), h.GetPairFormat(asset.Spot, true)), Timestamp: time.Unix(0, trade.Tick.Timestamp*int64(time.Millisecond)), } - case strings.Contains(init.Channel, "detail"): + case strings.Contains(init.Channel, "detail"), + strings.Contains(init.Rep, "detail"): var wsTicker WsTick - err := json.Unmarshal(resp.Raw, &wsTicker) + err := json.Unmarshal(respRaw, &wsTicker) if err != nil { - h.Websocket.DataHandler <- err - return + return err + } + var data []string + if wsTicker.Channel != "" { + data = strings.Split(wsTicker.Channel, ".") + } + if wsTicker.Rep != "" { + data = strings.Split(wsTicker.Rep, ".") } - data := strings.Split(wsTicker.Channel, ".") h.Websocket.DataHandler <- &ticker.Price{ ExchangeName: h.Name, Open: wsTicker.Tick.Open, @@ -292,7 +380,11 @@ func (h *HUOBI) wsHandleMarketData(resp WsMessage) { Pair: currency.NewPairFromFormattedPairs(data[1], h.GetEnabledPairs(asset.Spot), h.GetPairFormat(asset.Spot, true)), } + default: + h.Websocket.DataHandler <- wshandler.UnhandledMessageWarning{Message: h.Name + wshandler.UnhandledMessage + string(respRaw)} + return nil } + return nil } func (h *HUOBI) sendPingResponse(pong int64) { diff --git a/exchanges/huobi/huobi_wrapper.go b/exchanges/huobi/huobi_wrapper.go index 12b97856..b6d2985f 100644 --- a/exchanges/huobi/huobi_wrapper.go +++ b/exchanges/huobi/huobi_wrapper.go @@ -102,6 +102,7 @@ func (h *HUOBI) SetDefaults() { MessageCorrelation: true, GetOrder: true, GetOrders: true, + TickerFetching: true, }, WithdrawPermissions: exchange.AutoWithdrawCryptoWithSetup | exchange.NoFiatWithdrawals, @@ -522,14 +523,14 @@ func (h *HUOBI) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) { } switch { - case s.OrderSide == order.Buy && s.OrderType == order.Market: + case s.Side == order.Buy && s.Type == order.Market: formattedType = SpotNewOrderRequestTypeBuyMarket - case s.OrderSide == order.Sell && s.OrderType == order.Market: + case s.Side == order.Sell && s.Type == order.Market: formattedType = SpotNewOrderRequestTypeSellMarket - case s.OrderSide == order.Buy && s.OrderType == order.Limit: + case s.Side == order.Buy && s.Type == order.Limit: formattedType = SpotNewOrderRequestTypeBuyLimit params.Price = s.Price - case s.OrderSide == order.Sell && s.OrderType == order.Limit: + case s.Side == order.Sell && s.Type == order.Limit: formattedType = SpotNewOrderRequestTypeSellLimit params.Price = s.Price } @@ -544,7 +545,7 @@ func (h *HUOBI) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) { } submitOrderResponse.IsOrderPlaced = true - if s.OrderType == order.Market { + if s.Type == order.Market { submitOrderResponse.FullyMatched = true } return submitOrderResponse, nil @@ -558,7 +559,7 @@ func (h *HUOBI) ModifyOrder(action *order.Modify) (string, error) { // CancelOrder cancels an order by its corresponding ID number func (h *HUOBI) CancelOrder(order *order.Cancel) error { - orderIDInt, err := strconv.ParseInt(order.OrderID, 10, 64) + orderIDInt, err := strconv.ParseInt(order.ID, 10, 64) if err != nil { return err } @@ -616,33 +617,68 @@ func (h *HUOBI) GetOrderInfo(orderID string) (order.Detail, error) { if respData.ID == 0 { return orderDetail, fmt.Errorf("%s - order not found for orderid %s", h.Name, orderID) } - + var responseID = strconv.FormatInt(respData.ID, 10) + if responseID != orderID { + return orderDetail, errors.New(h.Name + " - GetOrderInfo orderID mismatch. Expected: " + orderID + " Received: " + responseID) + } typeDetails := strings.Split(respData.Type, "-") orderSide, err := order.StringToOrderSide(typeDetails[0]) if err != nil { - return orderDetail, err + if h.Websocket.IsConnected() { + h.Websocket.DataHandler <- order.ClassificationError{ + Exchange: h.Name, + OrderID: orderID, + Err: err, + } + } else { + return orderDetail, err + } } orderType, err := order.StringToOrderType(typeDetails[1]) if err != nil { - return orderDetail, err + if h.Websocket.IsConnected() { + h.Websocket.DataHandler <- order.ClassificationError{ + Exchange: h.Name, + OrderID: orderID, + Err: err, + } + } else { + return orderDetail, err + } } orderStatus, err := order.StringToOrderStatus(respData.State) + if err != nil { + if h.Websocket.IsConnected() { + h.Websocket.DataHandler <- order.ClassificationError{ + Exchange: h.Name, + OrderID: orderID, + Err: err, + } + } else { + return orderDetail, err + } + } + var p currency.Pair + var a asset.Item + p, a, err = h.GetRequestFormattedPairAndAssetType(respData.Symbol) if err != nil { return orderDetail, err } + orderDetail = order.Detail{ Exchange: h.Name, - ID: strconv.FormatInt(respData.ID, 10), + ID: orderID, AccountID: strconv.FormatInt(respData.AccountID, 10), - CurrencyPair: currency.NewPairFromString(respData.Symbol), - OrderType: orderType, - OrderSide: orderSide, - OrderDate: time.Unix(0, respData.CreatedAt*int64(time.Millisecond)), + Pair: p, + Type: orderType, + Side: orderSide, + Date: time.Unix(0, respData.CreatedAt*int64(time.Millisecond)), Status: orderStatus, Price: respData.Price, Amount: respData.Amount, ExecutedAmount: respData.FilledAmount, Fee: respData.FilledFees, + AssetType: a, } return orderDetail, nil } @@ -693,48 +729,61 @@ func (h *HUOBI) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) { // GetActiveOrders retrieves any orders that are active/open func (h *HUOBI) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, error) { - if len(req.Currencies) == 0 { + if len(req.Pairs) == 0 { return nil, errors.New("currency must be supplied") } side := "" - if req.OrderSide == order.AnySide || req.OrderSide == "" { + if req.Side == order.AnySide || req.Side == "" { side = "" - } else if req.OrderSide == order.Sell { - side = req.OrderSide.Lower() + } else if req.Side == order.Sell { + side = req.Side.Lower() } var orders []order.Detail if h.Websocket.CanUseAuthenticatedWebsocketForWrapper() { - for i := range req.Currencies { - resp, err := h.wsGetOrdersList(-1, req.Currencies[i]) + for i := range req.Pairs { + resp, err := h.wsGetOrdersList(-1, req.Pairs[i]) if err != nil { return orders, err } for j := range resp.Data { sideData := strings.Split(resp.Data[j].OrderState, "-") side = sideData[0] + var orderID = strconv.FormatInt(resp.Data[j].OrderID, 10) orderSide, err := order.StringToOrderSide(side) if err != nil { - return orders, err + h.Websocket.DataHandler <- order.ClassificationError{ + Exchange: h.Name, + OrderID: orderID, + Err: err, + } } orderType, err := order.StringToOrderType(sideData[1]) if err != nil { - return orders, err + h.Websocket.DataHandler <- order.ClassificationError{ + Exchange: h.Name, + OrderID: orderID, + Err: err, + } } orderStatus, err := order.StringToOrderStatus(resp.Data[j].OrderState) if err != nil { - return orders, err + h.Websocket.DataHandler <- order.ClassificationError{ + Exchange: h.Name, + OrderID: orderID, + Err: err, + } } orders = append(orders, order.Detail{ Exchange: h.Name, AccountID: strconv.FormatInt(resp.Data[j].AccountID, 10), - ID: strconv.FormatInt(resp.Data[j].OrderID, 10), - CurrencyPair: req.Currencies[i], - OrderType: orderType, - OrderSide: orderSide, - OrderDate: time.Unix(0, resp.Data[j].CreatedAt*int64(time.Millisecond)), + ID: orderID, + Pair: req.Pairs[i], + Type: orderType, + Side: orderSide, + Date: time.Unix(0, resp.Data[j].CreatedAt*int64(time.Millisecond)), Status: orderStatus, Price: resp.Data[j].Price, Amount: resp.Data[j].OrderAmount, @@ -745,9 +794,9 @@ func (h *HUOBI) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, er } } } else { - for i := range req.Currencies { + for i := range req.Pairs { resp, err := h.GetOpenOrders(h.API.Credentials.ClientID, - req.Currencies[i].Lower().String(), + req.Pairs[i].Lower().String(), side, 500) if err != nil { @@ -759,10 +808,10 @@ func (h *HUOBI) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, er ID: strconv.FormatInt(resp[i].ID, 10), Price: resp[i].Price, Amount: resp[i].Amount, - CurrencyPair: req.Currencies[i], + Pair: req.Pairs[i], Exchange: h.Name, ExecutedAmount: resp[i].FilledAmount, - OrderDate: time.Unix(0, resp[i].CreatedAt*int64(time.Millisecond)), + Date: time.Unix(0, resp[i].CreatedAt*int64(time.Millisecond)), Status: order.Status(resp[i].State), AccountID: strconv.FormatInt(resp[i].AccountID, 10), Fee: resp[i].FilledFees, @@ -782,14 +831,14 @@ func (h *HUOBI) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, er // GetOrderHistory retrieves account order information // Can Limit response to specific order status func (h *HUOBI) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail, error) { - if len(req.Currencies) == 0 { + if len(req.Pairs) == 0 { return nil, errors.New("currency must be supplied") } states := "partial-canceled,filled,canceled" var orders []order.Detail - for i := range req.Currencies { - resp, err := h.GetOrders(req.Currencies[i].Lower().String(), + for i := range req.Pairs { + resp, err := h.GetOrders(req.Pairs[i].Lower().String(), "", "", "", @@ -806,10 +855,10 @@ func (h *HUOBI) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail, er ID: strconv.FormatInt(resp[i].ID, 10), Price: resp[i].Price, Amount: resp[i].Amount, - CurrencyPair: req.Currencies[i], + Pair: req.Pairs[i], Exchange: h.Name, ExecutedAmount: resp[i].FilledAmount, - OrderDate: time.Unix(0, resp[i].CreatedAt*int64(time.Millisecond)), + Date: time.Unix(0, resp[i].CreatedAt*int64(time.Millisecond)), Status: order.Status(resp[i].State), AccountID: strconv.FormatInt(resp[i].AccountID, 10), Fee: resp[i].FilledFees, @@ -828,17 +877,17 @@ func (h *HUOBI) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail, er func setOrderSideAndType(requestType string, orderDetail *order.Detail) { switch SpotNewOrderRequestParamsType(requestType) { case SpotNewOrderRequestTypeBuyMarket: - orderDetail.OrderSide = order.Buy - orderDetail.OrderType = order.Market + orderDetail.Side = order.Buy + orderDetail.Type = order.Market case SpotNewOrderRequestTypeSellMarket: - orderDetail.OrderSide = order.Sell - orderDetail.OrderType = order.Market + orderDetail.Side = order.Sell + orderDetail.Type = order.Market case SpotNewOrderRequestTypeBuyLimit: - orderDetail.OrderSide = order.Buy - orderDetail.OrderType = order.Limit + orderDetail.Side = order.Buy + orderDetail.Type = order.Limit case SpotNewOrderRequestTypeSellLimit: - orderDetail.OrderSide = order.Sell - orderDetail.OrderType = order.Limit + orderDetail.Side = order.Sell + orderDetail.Type = order.Limit } } diff --git a/exchanges/itbit/itbit_test.go b/exchanges/itbit/itbit_test.go index 027d0ba0..d4ee9679 100644 --- a/exchanges/itbit/itbit_test.go +++ b/exchanges/itbit/itbit_test.go @@ -267,7 +267,7 @@ func TestFormatWithdrawPermissions(t *testing.T) { func TestGetActiveOrders(t *testing.T) { var getOrdersRequest = order.GetOrdersRequest{ - OrderType: order.AnyType, + Type: order.AnyType, } _, err := i.GetActiveOrders(&getOrdersRequest) @@ -280,7 +280,7 @@ func TestGetActiveOrders(t *testing.T) { func TestGetOrderHistory(t *testing.T) { var getOrdersRequest = order.GetOrdersRequest{ - OrderType: order.AnyType, + Type: order.AnyType, } _, err := i.GetOrderHistory(&getOrdersRequest) @@ -307,11 +307,11 @@ func TestSubmitOrder(t *testing.T) { Base: currency.BTC, Quote: currency.USD, }, - OrderSide: order.Buy, - OrderType: order.Limit, - Price: 1, - Amount: 1, - ClientID: "meowOrder", + Side: order.Buy, + Type: order.Limit, + Price: 1, + Amount: 1, + ClientID: "meowOrder", } response, err := i.SubmitOrder(orderSubmission) if areTestAPIKeysSet() && (err != nil || !response.IsOrderPlaced) { @@ -328,10 +328,10 @@ func TestCancelExchangeOrder(t *testing.T) { currencyPair := currency.NewPair(currency.LTC, currency.BTC) var orderCancellation = &order.Cancel{ - OrderID: "1", + ID: "1", WalletAddress: core.BitcoinDonationAddress, AccountID: "1", - CurrencyPair: currencyPair, + Pair: currencyPair, } err := i.CancelOrder(orderCancellation) @@ -351,10 +351,10 @@ func TestCancelAllExchangeOrders(t *testing.T) { currencyPair := currency.NewPair(currency.LTC, currency.BTC) var orderCancellation = &order.Cancel{ - OrderID: "1", + ID: "1", WalletAddress: core.BitcoinDonationAddress, AccountID: "1", - CurrencyPair: currencyPair, + Pair: currencyPair, } resp, err := i.CancelAllOrders(orderCancellation) diff --git a/exchanges/itbit/itbit_wrapper.go b/exchanges/itbit/itbit_wrapper.go index 51e48cf6..826b7574 100644 --- a/exchanges/itbit/itbit_wrapper.go +++ b/exchanges/itbit/itbit_wrapper.go @@ -340,8 +340,8 @@ func (i *ItBit) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) { } response, err := i.PlaceOrder(wallet, - s.OrderSide.String(), - s.OrderType.String(), + s.Side.String(), + s.Type.String(), s.Pair.Base.String(), s.Amount, s.Price, @@ -369,7 +369,7 @@ func (i *ItBit) ModifyOrder(action *order.Modify) (string, error) { // CancelOrder cancels an order by its corresponding ID number func (i *ItBit) CancelOrder(order *order.Cancel) error { - return i.CancelExistingOrder(order.WalletAddress, order.OrderID) + return i.CancelExistingOrder(order.WalletAddress, order.ID) } // CancelAllOrders cancels all orders associated with a currency pair @@ -471,19 +471,19 @@ func (i *ItBit) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, er orders = append(orders, order.Detail{ ID: allOrders[j].ID, - OrderSide: side, + Side: side, Amount: allOrders[j].Amount, ExecutedAmount: allOrders[j].AmountFilled, RemainingAmount: (allOrders[j].Amount - allOrders[j].AmountFilled), Exchange: i.Name, - OrderDate: orderDate, - CurrencyPair: symbol, + Date: orderDate, + Pair: symbol, }) } order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) - order.FilterOrdersBySide(&orders, req.OrderSide) - order.FilterOrdersByCurrencies(&orders, req.Currencies) + order.FilterOrdersBySide(&orders, req.Side) + order.FilterOrdersByCurrencies(&orders, req.Pairs) return orders, nil } @@ -525,19 +525,19 @@ func (i *ItBit) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail, er orders = append(orders, order.Detail{ ID: allOrders[j].ID, - OrderSide: side, + Side: side, Amount: allOrders[j].Amount, ExecutedAmount: allOrders[j].AmountFilled, RemainingAmount: (allOrders[j].Amount - allOrders[j].AmountFilled), Exchange: i.Name, - OrderDate: orderDate, - CurrencyPair: symbol, + Date: orderDate, + Pair: symbol, }) } order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) - order.FilterOrdersBySide(&orders, req.OrderSide) - order.FilterOrdersByCurrencies(&orders, req.Currencies) + order.FilterOrdersBySide(&orders, req.Side) + order.FilterOrdersByCurrencies(&orders, req.Pairs) return orders, nil } diff --git a/exchanges/kraken/kraken_test.go b/exchanges/kraken/kraken_test.go index fbb484ac..cad8da64 100644 --- a/exchanges/kraken/kraken_test.go +++ b/exchanges/kraken/kraken_test.go @@ -50,7 +50,8 @@ func TestMain(m *testing.M) { if err != nil { log.Fatal("Kraken setup error", err) } - + k.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride() + k.Websocket.TrafficAlert = sharedtestvalues.GetWebsocketStructChannelOverride() os.Exit(m.Run()) } @@ -381,7 +382,7 @@ func TestFormatWithdrawPermissions(t *testing.T) { // TestGetActiveOrders wrapper test func TestGetActiveOrders(t *testing.T) { var getOrdersRequest = order.GetOrdersRequest{ - OrderType: order.AnyType, + Type: order.AnyType, } _, err := k.GetActiveOrders(&getOrdersRequest) @@ -395,7 +396,7 @@ func TestGetActiveOrders(t *testing.T) { // TestGetOrderHistory wrapper test func TestGetOrderHistory(t *testing.T) { var getOrdersRequest = order.GetOrdersRequest{ - OrderType: order.AnyType, + Type: order.AnyType, } _, err := k.GetOrderHistory(&getOrdersRequest) @@ -438,11 +439,11 @@ func TestSubmitOrder(t *testing.T) { Base: currency.XBT, Quote: currency.USD, }, - OrderSide: order.Buy, - OrderType: order.Limit, - Price: 1, - Amount: 1, - ClientID: "meowOrder", + Side: order.Buy, + Type: order.Limit, + Price: 1, + Amount: 1, + ClientID: "meowOrder", } response, err := k.SubmitOrder(orderSubmission) if areTestAPIKeysSet() && (err != nil || !response.IsOrderPlaced) { @@ -460,10 +461,10 @@ func TestCancelExchangeOrder(t *testing.T) { currencyPair := currency.NewPair(currency.LTC, currency.BTC) var orderCancellation = &order.Cancel{ - OrderID: "1", + ID: "1", WalletAddress: core.BitcoinDonationAddress, AccountID: "1", - CurrencyPair: currencyPair, + Pair: currencyPair, } err := k.CancelOrder(orderCancellation) @@ -483,10 +484,10 @@ func TestCancelAllExchangeOrders(t *testing.T) { currencyPair := currency.NewPair(currency.LTC, currency.BTC) var orderCancellation = &order.Cancel{ - OrderID: "1", + ID: "1", WalletAddress: core.BitcoinDonationAddress, AccountID: "1", - CurrencyPair: currencyPair, + Pair: currencyPair, } resp, err := k.CancelAllOrders(orderCancellation) @@ -680,9 +681,9 @@ func setupWsTests(t *testing.T) { } authToken = token - go k.WsReadData(k.WebsocketConn) - go k.WsReadData(k.AuthenticatedWebsocketConn) - go k.WsHandleData() + go k.wsFunnelConnectionData(k.WebsocketConn) + go k.wsFunnelConnectionData(k.AuthenticatedWebsocketConn) + go k.wsReadData() go k.wsPingHandler() wsSetupRan = true } @@ -733,3 +734,624 @@ func TestWsCancelOrder(t *testing.T) { t.Error(err) } } + +func TestWsPong(t *testing.T) { + pressXToJSON := []byte(`{ + "event": "pong", + "reqid": 42 +}`) + err := k.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsSystemStatus(t *testing.T) { + pressXToJSON := []byte(`{ + "connectionID": 8628615390848610000, + "event": "systemStatus", + "status": "online", + "version": "1.0.0" +}`) + err := k.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsSubscriptionStatus(t *testing.T) { + pressXToJSON := []byte(`{ + "channelID": 10001, + "channelName": "ticker", + "event": "subscriptionStatus", + "pair": "XBT/EUR", + "status": "subscribed", + "subscription": { + "name": "ticker" + } +}`) + err := k.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } + + pressXToJSON = []byte(`{ + "channelID": 10001, + "channelName": "ohlc-5", + "event": "subscriptionStatus", + "pair": "XBT/EUR", + "reqid": 42, + "status": "unsubscribed", + "subscription": { + "interval": 5, + "name": "ohlc" + } +}`) + err = k.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } + + pressXToJSON = []byte(`{ + "channelName": "ownTrades", + "event": "subscriptionStatus", + "status": "subscribed", + "subscription": { + "name": "ownTrades" + } +}`) + err = k.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } + pressXToJSON = []byte(`{ + "errorMessage": "Subscription depth not supported", + "event": "subscriptionStatus", + "pair": "XBT/USD", + "status": "error", + "subscription": { + "depth": 42, + "name": "book" + } +}`) + err = k.wsHandleData(pressXToJSON) + if err == nil { + t.Error("Expected error") + } +} + +func TestWsTicker(t *testing.T) { + pressXToJSON := []byte(`{ + "channelID": 1337, + "channelName": "ticker", + "event": "subscriptionStatus", + "pair": "XBT/EUR", + "status": "subscribed", + "subscription": { + "name": "ticker" + } +}`) + err := k.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } + pressXToJSON = []byte(`[ + 1337, + { + "a": [ + "5525.40000", + 1, + "1.000" + ], + "b": [ + "5525.10000", + 1, + "1.000" + ], + "c": [ + "5525.10000", + "0.00398963" + ], + "h": [ + "5783.00000", + "5783.00000" + ], + "l": [ + "5505.00000", + "5505.00000" + ], + "o": [ + "5760.70000", + "5763.40000" + ], + "p": [ + "5631.44067", + "5653.78939" + ], + "t": [ + 11493, + 16267 + ], + "v": [ + "2634.11501494", + "3591.17907851" + ] + }, + "ticker", + "XBT/USD" +]`) + err = k.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsOHLC(t *testing.T) { + pressXToJSON := []byte(`{ + "channelID": 13337, + "channelName": "ohlc", + "event": "subscriptionStatus", + "pair": "XBT/EUR", + "status": "subscribed", + "subscription": { + "name": "ohlc" + } +}`) + err := k.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } + pressXToJSON = []byte(`[ + 13337, + [ + "1542057314.748456", + "1542057360.435743", + "3586.70000", + "3586.70000", + "3586.60000", + "3586.60000", + "3586.68894", + "0.03373000", + 2 + ], + "ohlc-5", + "XBT/USD" +]`) + err = k.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsTrade(t *testing.T) { + pressXToJSON := []byte(`{ + "channelID": 133337, + "channelName": "trade", + "event": "subscriptionStatus", + "pair": "XBT/EUR", + "status": "subscribed", + "subscription": { + "name": "trade" + } +}`) + err := k.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } + pressXToJSON = []byte(`[ + 133337, + [ + [ + "5541.20000", + "0.15850568", + "1534614057.321597", + "s", + "l", + "" + ], + [ + "6060.00000", + "0.02455000", + "1534614057.324998", + "b", + "l", + "" + ] + ], + "trade", + "XBT/USD" +]`) + err = k.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsSpread(t *testing.T) { + pressXToJSON := []byte(`{ + "channelID": 1333337, + "channelName": "spread", + "event": "subscriptionStatus", + "pair": "XBT/EUR", + "status": "subscribed", + "subscription": { + "name": "spread" + } +}`) + err := k.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } + pressXToJSON = []byte(`[ + 1333337, + [ + "5698.40000", + "5700.00000", + "1542057299.545897", + "1.01234567", + "0.98765432" + ], + "spread", + "XBT/USD" +]`) + err = k.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsOrdrbook(t *testing.T) { + pressXToJSON := []byte(`{ + "channelID": 13333337, + "channelName": "book", + "event": "subscriptionStatus", + "pair": "XBT/EUR", + "status": "subscribed", + "subscription": { + "name": "book" + } +}`) + err := k.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } + pressXToJSON = []byte(`[ + 13333337, + { + "as": [ + [ + "5541.30000", + "2.50700000", + "1534614248.123678" + ], + [ + "5541.80000", + "0.33000000", + "1534614098.345543" + ], + [ + "5542.70000", + "0.64700000", + "1534614244.654432" + ] + ], + "bs": [ + [ + "5541.20000", + "1.52900000", + "1534614248.765567" + ], + [ + "5539.90000", + "0.30000000", + "1534614241.769870" + ], + [ + "5539.50000", + "5.00000000", + "1534613831.243486" + ] + ] + }, + "book-100", + "XBT/USD" +]`) + err = k.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } + pressXToJSON = []byte(`[ + 13333337, + { + "a": [ + [ + "5541.30000", + "2.50700000", + "1534614248.456738" + ], + [ + "5542.50000", + "0.40100000", + "1534614248.456738" + ] + ] + }, + "book-10", + "XBT/USD" +]`) + err = k.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } + pressXToJSON = []byte(`[ + 13333337, + { + "b": [ + [ + "5541.30000", + "0.00000000", + "1534614335.345903" + ] + ] + }, + "book-10", + "XBT/USD" +]`) + err = k.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsOwnTrades(t *testing.T) { + pressXToJSON := []byte(`[ + [ + { + "TDLH43-DVQXD-2KHVYY": { + "cost": "1000000.00000", + "fee": "1600.00000", + "margin": "0.00000", + "ordertxid": "TDLH43-DVQXD-2KHVYY", + "ordertype": "limit", + "pair": "XBT/USD", + "postxid": "OGTT3Y-C6I3P-XRI6HX", + "price": "100000.00000", + "time": "1560516023.070651", + "type": "sell", + "vol": "1000000000.00000000" + } + }, + { + "TDLH43-DVQXD-2KHVYY": { + "cost": "1000000.00000", + "fee": "600.00000", + "margin": "0.00000", + "ordertxid": "TDLH43-DVQXD-2KHVYY", + "ordertype": "limit", + "pair": "XBT/USD", + "postxid": "OGTT3Y-C6I3P-XRI6HX", + "price": "100000.00000", + "time": "1560516023.070658", + "type": "buy", + "vol": "1000000000.00000000" + } + }, + { + "TDLH43-DVQXD-2KHVYY": { + "cost": "1000000.00000", + "fee": "1600.00000", + "margin": "0.00000", + "ordertxid": "TDLH43-DVQXD-2KHVYY", + "ordertype": "limit", + "pair": "XBT/USD", + "postxid": "OGTT3Y-C6I3P-XRI6HX", + "price": "100000.00000", + "time": "1560520332.914657", + "type": "sell", + "vol": "1000000000.00000000" + } + }, + { + "TDLH43-DVQXD-2KHVYY": { + "cost": "1000000.00000", + "fee": "600.00000", + "margin": "0.00000", + "ordertxid": "TDLH43-DVQXD-2KHVYY", + "ordertype": "limit", + "pair": "XBT/USD", + "postxid": "OGTT3Y-C6I3P-XRI6HX", + "price": "100000.00000", + "time": "1560520332.914664", + "type": "buy", + "vol": "1000000000.00000000" + } + } + ], + "ownTrades" +]`) + err := k.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsOpenOrders(t *testing.T) { + pressXToJSON := []byte(`[ + [ + { + "OGTT3Y-C6I3P-XRI6HX": { + "cost": "0.00000", + "descr": { + "close": "", + "leverage": "0:1", + "order": "sell 10.00345345 XBT/USD @ limit 34.50000 with 0:1 leverage", + "ordertype": "limit", + "pair": "XBT/USD", + "price": "34.50000", + "price2": "0.00000", + "type": "sell" + }, + "expiretm": "0.000000", + "fee": "0.00000", + "limitprice": "34.50000", + "misc": "", + "oflags": "fcib", + "opentm": "0.000000", + "price": "34.50000", + "refid": "OKIVMP-5GVZN-Z2D2UA", + "starttm": "0.000000", + "status": "open", + "stopprice": "0.000000", + "userref": 0, + "vol": "10.00345345", + "vol_exec": "0.00000000" + } + }, + { + "OGTT3Y-C6I3P-XRI6HX": { + "cost": "0.00000", + "descr": { + "close": "", + "leverage": "0:1", + "order": "sell 0.00000010 XBT/USD @ limit 5334.60000 with 0:1 leverage", + "ordertype": "limit", + "pair": "XBT/USD", + "price": "5334.60000", + "price2": "0.00000", + "type": "sell" + }, + "expiretm": "0.000000", + "fee": "0.00000", + "limitprice": "5334.60000", + "misc": "", + "oflags": "fcib", + "opentm": "0.000000", + "price": "5334.60000", + "refid": "OKIVMP-5GVZN-Z2D2UA", + "starttm": "0.000000", + "status": "open", + "stopprice": "0.000000", + "userref": 0, + "vol": "0.00000010", + "vol_exec": "0.00000000" + } + }, + { + "OGTT3Y-C6I3P-XRI6HX": { + "cost": "0.00000", + "descr": { + "close": "", + "leverage": "0:1", + "order": "sell 0.00001000 XBT/USD @ limit 90.40000 with 0:1 leverage", + "ordertype": "limit", + "pair": "XBT/USD", + "price": "90.40000", + "price2": "0.00000", + "type": "sell" + }, + "expiretm": "0.000000", + "fee": "0.00000", + "limitprice": "90.40000", + "misc": "", + "oflags": "fcib", + "opentm": "0.000000", + "price": "90.40000", + "refid": "OKIVMP-5GVZN-Z2D2UA", + "starttm": "0.000000", + "status": "open", + "stopprice": "0.000000", + "userref": 0, + "vol": "0.00001000", + "vol_exec": "0.00000000" + } + }, + { + "OGTT3Y-C6I3P-XRI6HX": { + "cost": "0.00000", + "descr": { + "close": "", + "leverage": "0:1", + "order": "sell 0.00001000 XBT/USD @ limit 9.00000 with 0:1 leverage", + "ordertype": "limit", + "pair": "XBT/USD", + "price": "9.00000", + "price2": "0.00000", + "type": "sell" + }, + "expiretm": "0.000000", + "fee": "0.00000", + "limitprice": "9.00000", + "misc": "", + "oflags": "fcib", + "opentm": "0.000000", + "price": "9.00000", + "refid": "OKIVMP-5GVZN-Z2D2UA", + "starttm": "0.000000", + "status": "open", + "stopprice": "0.000000", + "userref": 0, + "vol": "0.00001000", + "vol_exec": "0.00000000" + } + } + ], + "openOrders" +]`) + err := k.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } + pressXToJSON = []byte(`[ + [ + { + "OGTT3Y-C6I3P-XRI6HX": { + "status": "closed" + } + }, + { + "OGTT3Y-C6I3P-XRI6HX": { + "status": "closed" + } + } + ], + "openOrders" +]`) + err = k.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsAddOrderJSON(t *testing.T) { + pressXToJSON := []byte(`{ + "descr": "buy 0.01770000 XBTUSD @ limit 4000", + "event": "addOrderStatus", + "status": "ok", + "txid": "ONPNXH-KMKMU-F4MR5V" +}`) + err := k.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } + + pressXToJSON = []byte(`{ + "errorMessage": "EOrder:Order minimum not met", + "event": "addOrderStatus", + "status": "error" +}`) + err = k.wsHandleData(pressXToJSON) + if err == nil { + t.Error("Expected error") + } +} + +func TestWsCancelOrderJSON(t *testing.T) { + pressXToJSON := []byte(`{ + "event": "cancelOrderStatus", + "status": "ok" +}`) + err := k.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} diff --git a/exchanges/kraken/kraken_types.go b/exchanges/kraken/kraken_types.go index d950a6a9..0bc0162c 100644 --- a/exchanges/kraken/kraken_types.go +++ b/exchanges/kraken/kraken_types.go @@ -430,7 +430,6 @@ type WebsocketEventResponse struct { Subscription WebsocketSubscriptionResponseData `json:"subscription,omitempty"` ChannelName string `json:"channelName,omitempty"` WebsocketSubscriptionEventResponse - WebsocketStatusResponse WebsocketErrorResponse } @@ -444,12 +443,6 @@ type WebsocketSubscriptionResponseData struct { Name string `json:"name"` } -// WebsocketStatusResponse defines a websocket status response -type WebsocketStatusResponse struct { - ConnectionID float64 `json:"connectionID"` - Version string `json:"version"` -} - // WebsocketDataResponse defines a websocket data type type WebsocketDataResponse []interface{} @@ -475,19 +468,70 @@ type WsTokenResponse struct { } `json:"result"` } +type wsSystemStatus struct { + ConnectionID float64 `json:"connectionID"` + Event string `json:"event"` + Status string `json:"status"` + Version string `json:"version"` +} + +type wsSubscription struct { + ChannelID int64 `json:"channelID"` + ChannelName string `json:"channelName"` + ErrorMessage string `json:"errorMessage"` + Event string `json:"event"` + Pair string `json:"pair"` + RequestID int64 `json:"reqid"` + Status string `json:"status"` + Subscription struct { + Depth int `json:"depth"` + Interval int `json:"interval"` + Name string `json:"name"` + } `json:"subscription"` +} + +// WsOpenOrder contains all open order data from ws feed +type WsOpenOrder struct { + UserReferenceID int64 `json:"userref"` + ExpireTime float64 `json:"expiretm,string"` + OpenTime float64 `json:"opentm,string"` + StartTime float64 `json:"starttm,string"` + Fee float64 `json:"fee,string"` + LimitPrice float64 `json:"limitprice,string"` + StopPrice float64 `json:"stopprice,string"` + Volume float64 `json:"vol,string"` + ExecutedVolume float64 `json:"vol_exec,string"` + Cost float64 `json:"cost,string"` + Price float64 `json:"price,string"` + Misc string `json:"misc"` + OFlags string `json:"oflags"` + RefID string `json:"refid"` + Status string `json:"status"` + Description struct { + Close string `json:"close"` + Price float64 `json:"price,string"` + Price2 float64 `json:"price2,string"` + Leverage string `json:"leverage"` + Order string `json:"order"` + OrderType string `json:"ordertype"` + Pair string `json:"pair"` + Type string `json:"type"` + } `json:"descr"` +} + // WsOwnTrade ws auth owntrade data type WsOwnTrade struct { - Cost float64 `json:"cost,string"` - Fee float64 `json:"fee,string"` - Margin float64 `json:"margin,string"` - OrderTransactionID string `json:"ordertxid"` - OrderType string `json:"ordertype"` - Pair string `json:"pair"` - PostTransactionID string `json:"postxid"` - Price float64 `json:"price,string"` - Time time.Time `json:"time"` - Type string `json:"type"` - Vol float64 `json:"vol,string"` + Cost float64 `json:"cost,string"` + Fee float64 `json:"fee,string"` + Margin float64 `json:"margin,string"` + OrderTransactionID string `json:"ordertxid"` + OrderType string `json:"ordertype"` + Pair string `json:"pair"` + PostTransactionID string `json:"postxid"` + Price float64 `json:"price,string"` + Time float64 `json:"time,string"` + Type string `json:"type"` + Vol float64 `json:"vol,string"` } // WsOpenOrders ws auth open order data diff --git a/exchanges/kraken/kraken_websocket.go b/exchanges/kraken/kraken_websocket.go index a3fa0b75..c280e651 100644 --- a/exchanges/kraken/kraken_websocket.go +++ b/exchanges/kraken/kraken_websocket.go @@ -7,6 +7,7 @@ import ( "math" "net/http" "strconv" + "strings" "time" "github.com/gorilla/websocket" @@ -14,6 +15,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" @@ -26,7 +28,7 @@ const ( krakenWSURL = "wss://ws.kraken.com" krakenAuthWSURL = "wss://ws-auth.kraken.com" krakenWSSandboxURL = "wss://sandbox.kraken.com" - krakenWSSupportedVersion = "0.3.0" + krakenWSSupportedVersion = "1.0.0" // WS endpoints krakenWsHeartbeat = "heartbeat" krakenWsSystemStatus = "systemStatus" @@ -42,6 +44,8 @@ const ( krakenWsOpenOrders = "openOrders" krakenWsAddOrder = "addOrder" krakenWsCancelOrder = "cancelOrder" + krakenWsAddOrderStatus = "addOrderStatus" + krakenWsCancelOrderStatus = "cancelOrderStatus" krakenWsRateLimit = 50 krakenWsPingDelay = time.Second * 27 ) @@ -78,12 +82,12 @@ func (k *Kraken) WsConnect() error { k.Websocket.SetCanUseAuthenticatedEndpoints(false) log.Errorf(log.ExchangeSys, "%v - failed to connect to authenticated endpoint: %v\n", k.Name, err) } - go k.WsReadData(k.AuthenticatedWebsocketConn) + go k.wsFunnelConnectionData(k.AuthenticatedWebsocketConn) k.GenerateAuthenticatedSubscriptions() } - go k.WsReadData(k.WebsocketConn) - go k.WsHandleData() + go k.wsFunnelConnectionData(k.WebsocketConn) + go k.wsReadData() err = k.wsPingHandler() if err != nil { log.Errorf(log.ExchangeSys, "%v - failed setup ping handler. Websocket may disconnect unexpectedly. %v\n", k.Name, err) @@ -93,8 +97,8 @@ func (k *Kraken) WsConnect() error { return nil } -// WsReadData funnels both auth and public ws data into one manageable place -func (k *Kraken) WsReadData(ws *wshandler.WebsocketConnection) { +// wsFunnelConnectionData funnels both auth and public ws data into one manageable place +func (k *Kraken) wsFunnelConnectionData(ws *wshandler.WebsocketConnection) { k.Websocket.Wg.Add(1) defer k.Websocket.Wg.Done() for { @@ -113,8 +117,8 @@ func (k *Kraken) WsReadData(ws *wshandler.WebsocketConnection) { } } -// WsHandleData handles the read data from the websocket connection -func (k *Kraken) WsHandleData() { +// wsReadData receives and passes on websocket messages for processing +func (k *Kraken) wsReadData() { k.Websocket.Wg.Add(1) defer func() { k.Websocket.Wg.Done() @@ -126,30 +130,96 @@ func (k *Kraken) WsHandleData() { return default: resp := <-comms - // event response handling - var eventResponse WebsocketEventResponse - err := json.Unmarshal(resp.Raw, &eventResponse) - if err == nil && eventResponse.Event != "" { - k.WsHandleEventResponse(&eventResponse, resp.Raw) - continue - } - // Data response handling - var dataResponse WebsocketDataResponse - err = json.Unmarshal(resp.Raw, &dataResponse) + err := k.wsHandleData(resp.Raw) if err != nil { - log.Error(log.WebsocketMgr, fmt.Errorf("%s - unhandled websocket data: %v", k.Name, err)) - continue - } - if _, ok := dataResponse[0].(float64); ok { - k.WsHandleDataResponse(dataResponse) - } - if _, ok := dataResponse[1].(string); ok { - k.wsHandleAuthDataResponse(dataResponse) + k.Websocket.DataHandler <- fmt.Errorf("%s - unhandled websocket data: %v", k.Name, err) } } } } +func (k *Kraken) wsHandleData(respRaw []byte) error { + if strings.HasPrefix(string(respRaw), "[") { + var dataResponse WebsocketDataResponse + err := json.Unmarshal(respRaw, &dataResponse) + if err != nil { + return err + } + if _, ok := dataResponse[0].(float64); ok { + err = k.wsReadDataResponse(dataResponse) + if err != nil { + return err + } + } + if _, ok := dataResponse[1].(string); ok { + err = k.wsHandleAuthDataResponse(dataResponse) + if err != nil { + return err + } + } + } else { + var eventResponse map[string]interface{} + err := json.Unmarshal(respRaw, &eventResponse) + if err != nil { + return fmt.Errorf("%s - err %s could not parse websocket data: %s", k.Name, err, respRaw) + } + if event, ok := eventResponse["event"]; ok { + switch event { + case wshandler.Pong, krakenWsHeartbeat, krakenWsCancelOrderStatus: + return nil + case krakenWsSystemStatus: + var systemStatus wsSystemStatus + err := json.Unmarshal(respRaw, &systemStatus) + if err != nil { + return fmt.Errorf("%s - err %s unable to parse system status response: %s", k.Name, err, respRaw) + } + if systemStatus.Status != "online" { + k.Websocket.DataHandler <- fmt.Errorf("%v Websocket status '%v'", + k.Name, systemStatus.Status) + } + if systemStatus.Version > krakenWSSupportedVersion { + log.Warnf(log.ExchangeSys, "%v New version of Websocket API released. Was %v Now %v", + k.Name, krakenWSSupportedVersion, systemStatus.Version) + } + case krakenWsAddOrderStatus: + var status WsAddOrderResponse + err := json.Unmarshal(respRaw, &status) + if err != nil { + return fmt.Errorf("%s - err %s unable to parse add order response: %s", k.Name, err, respRaw) + } + if status.ErrorMessage != "" { + return fmt.Errorf("%s - err %s", k.Name, status.ErrorMessage) + } + k.Websocket.DataHandler <- &order.Detail{ + Exchange: k.Name, + ID: status.TransactionID, + Status: order.New, + } + case krakenWsSubscriptionStatus: + var sub wsSubscription + err := json.Unmarshal(respRaw, &sub) + if err != nil { + return fmt.Errorf("%s - err %s unable to parse subscription response: %s", k.Name, err, respRaw) + } + if sub.Status != "subscribed" && sub.Status != "unsubscribed" { + return fmt.Errorf("%v %v %v", k.Name, sub.RequestID, sub.ErrorMessage) + } + k.addNewSubscriptionChannelData(&sub) + if sub.RequestID > 0 { + if k.WebsocketConn.IsIDWaitingForResponse(sub.RequestID) { + k.WebsocketConn.SetResponseIDAndData(sub.RequestID, respRaw) + return nil + } + } + default: + k.Websocket.DataHandler <- wshandler.UnhandledMessageWarning{Message: k.Name + wshandler.UnhandledMessage + string(respRaw)} + } + return nil + } + } + return nil +} + // wsPingHandler sends a message "ping" every 27 to maintain the connection to the websocket func (k *Kraken) wsPingHandler() error { message, err := json.Marshal(pingRequest) @@ -164,278 +234,193 @@ func (k *Kraken) wsPingHandler() error { return nil } -// WsHandleDataResponse classifies the WS response and sends to appropriate handler -func (k *Kraken) WsHandleDataResponse(response WebsocketDataResponse) { +// wsReadDataResponse classifies the WS response and sends to appropriate handler +func (k *Kraken) wsReadDataResponse(response WebsocketDataResponse) error { if cID, ok := response[0].(float64); ok { channelID := int64(cID) channelData := getSubscriptionChannelData(channelID) switch channelData.Subscription { case krakenWsTicker: - if k.Verbose { - log.Debugf(log.ExchangeSys, "%v Websocket ticker data received", - k.Name) - } - k.wsProcessTickers(&channelData, response[1].(map[string]interface{})) + return 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].([]interface{})) + return 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].(map[string]interface{})) + return 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].([]interface{})) case krakenWsTrade: - if k.Verbose { - log.Debugf(log.ExchangeSys, "%v Websocket Trade data received", - k.Name) - } k.wsProcessTrades(&channelData, response[1].([]interface{})) default: - log.Errorf(log.ExchangeSys, "%v Unidentified websocket data received: %v", + return fmt.Errorf("%s Unidentified websocket data received: %+v", k.Name, response) } } + return nil } -// WsHandleEventResponse classifies the WS response and sends to appropriate handler -func (k *Kraken) WsHandleEventResponse(response *WebsocketEventResponse, rawResponse []byte) { - switch response.Event { - case wshandler.Pong: - break - case krakenWsHeartbeat: - if k.Verbose { - log.Debugf(log.ExchangeSys, "%v Websocket heartbeat data received", - k.Name) - } - case krakenWsSystemStatus: - if k.Verbose { - log.Debugf(log.ExchangeSys, "%v Websocket status data received", - k.Name) - } - if response.Status != "online" { - k.Websocket.DataHandler <- fmt.Errorf("%v Websocket status '%v'", - k.Name, response.Status) - } - if response.WebsocketStatusResponse.Version > krakenWSSupportedVersion { - log.Warnf(log.ExchangeSys, "%v New version of Websocket API released. Was %v Now %v", - k.Name, krakenWSSupportedVersion, response.WebsocketStatusResponse.Version) - } - case krakenWsSubscriptionStatus: - k.WebsocketConn.AddResponseWithID(response.RequestID, rawResponse) - if response.Status != "subscribed" { - k.Websocket.DataHandler <- fmt.Errorf("%v %v %v", k.Name, response.RequestID, response.WebsocketErrorResponse.ErrorMessage) - return - } - addNewSubscriptionChannelData(response) - default: - log.Errorf(log.ExchangeSys, "%v Unidentified websocket data received: %v", - k.Name, response) - } -} - -func (k *Kraken) wsHandleAuthDataResponse(response WebsocketDataResponse) { +func (k *Kraken) wsHandleAuthDataResponse(response WebsocketDataResponse) error { if chName, ok := response[1].(string); ok { switch chName { case krakenWsOwnTrades: - if k.Verbose { - log.Debugf(log.ExchangeSys, "%v Websocket auth own trade data received", - k.Name) - } - k.wsProcessOwnTrades(&response[0]) + return k.wsProcessOwnTrades(response[0]) case krakenWsOpenOrders: - if k.Verbose { - log.Debugf(log.ExchangeSys, "%v Websocket auth open order data received", - k.Name) - } - k.wsProcessOpenOrders(&response[0]) + return k.wsProcessOpenOrders(response[0]) + default: + return fmt.Errorf("%v Unidentified websocket data received: %+v", + k.Name, response) } } + return nil } -func (k *Kraken) wsProcessOwnTrades(ownOrders interface{}) { +func (k *Kraken) wsProcessOwnTrades(ownOrders interface{}) error { if data, ok := ownOrders.([]interface{}); ok { for i := range data { - ownTrade := data[i].(map[string]interface{}) - for _, val := range ownTrade { - tradeData := val.(map[string]interface{}) - cost, err := strconv.ParseFloat(tradeData["cost"].(string), 64) - if err != nil { - k.Websocket.DataHandler <- err - } - fee, err := strconv.ParseFloat(tradeData["fee"].(string), 64) - if err != nil { - k.Websocket.DataHandler <- err - } - margin, err := strconv.ParseFloat(tradeData["margin"].(string), 64) - if err != nil { - k.Websocket.DataHandler <- err - } - vol, err := strconv.ParseFloat(tradeData["vol"].(string), 64) - if err != nil { - k.Websocket.DataHandler <- err - } - price, err := strconv.ParseFloat(tradeData["price"].(string), 64) - if err != nil { - k.Websocket.DataHandler <- err - } - timeTogether, err := strconv.ParseFloat(tradeData["time"].(string), 64) - if err != nil { - k.Websocket.DataHandler <- err - } - first, second, err := convert.SplitFloatDecimals(timeTogether) - if err != nil { - k.Websocket.DataHandler <- err - } - k.Websocket.DataHandler <- WsOwnTrade{ - Cost: cost, - Fee: fee, - Margin: margin, - OrderTransactionID: tradeData["ordertxid"].(string), - OrderType: tradeData["ordertype"].(string), - Pair: tradeData["pair"].(string), - PostTransactionID: tradeData["postxid"].(string), - Price: price, - Time: time.Unix(first, second), - Type: tradeData["type"].(string), - Vol: vol, - } + trades, err := json.Marshal(data[i]) + if err != nil { + return err } - } - } else { - k.Websocket.DataHandler <- errors.New(k.Name + " - Invalid own trades data") - } -} - -func (k *Kraken) wsProcessOpenOrders(ownOrders interface{}) { - if data, ok := ownOrders.([]interface{}); ok { - for i := range data { - ownTrade := data[i].(map[string]interface{}) - for key, val := range ownTrade { - tradeData := val.(map[string]interface{}) - if len(tradeData) == 1 { - // just a status update - if status, ok := tradeData["status"].(string); ok { - k.Websocket.DataHandler <- k.Name + " - Order " + key + " " + status + var result map[string]*WsOwnTrade + err = json.Unmarshal(trades, &result) + if err != nil { + return err + } + for key, val := range result { + oSide, err := order.StringToOrderSide(val.Type) + if err != nil { + k.Websocket.DataHandler <- order.ClassificationError{ + Exchange: k.Name, + OrderID: key, + Err: err, } } - startTimeConv, err := strconv.ParseFloat(tradeData["starttm"].(string), 64) + oType, err := order.StringToOrderType(val.OrderType) if err != nil { - k.Websocket.DataHandler <- err + k.Websocket.DataHandler <- order.ClassificationError{ + Exchange: k.Name, + OrderID: key, + Err: err, + } } - startTime, startTimeNano, err := convert.SplitFloatDecimals(startTimeConv) + txTime, txTimeNano, err := convert.SplitFloatDecimals(val.Time) if err != nil { - k.Websocket.DataHandler <- err + return err } - openTimeConv, err := strconv.ParseFloat(tradeData["opentm"].(string), 64) - if err != nil { - k.Websocket.DataHandler <- err + trade := order.TradeHistory{ + Price: val.Price, + Amount: val.Vol, + Fee: val.Fee, + Exchange: k.Name, + TID: key, + Type: oType, + Side: oSide, + Timestamp: time.Unix(txTime, txTimeNano), } - openTime, openTimeNano, err := convert.SplitFloatDecimals(openTimeConv) - if err != nil { - k.Websocket.DataHandler <- err - } - expireTimeConv, err := strconv.ParseFloat(tradeData["expiretm"].(string), 64) - if err != nil { - k.Websocket.DataHandler <- err - } - expireTime, expireTimeNano, err := convert.SplitFloatDecimals(expireTimeConv) - if err != nil { - k.Websocket.DataHandler <- err - } - cost, err := strconv.ParseFloat(tradeData["cost"].(string), 64) - if err != nil { - k.Websocket.DataHandler <- err - } - executedVolume, err := strconv.ParseFloat(tradeData["vol_exec"].(string), 64) - if err != nil { - k.Websocket.DataHandler <- err - } - volume, err := strconv.ParseFloat(tradeData["vol"].(string), 64) - if err != nil { - k.Websocket.DataHandler <- err - } - userReference, err := strconv.ParseFloat(tradeData["userref"].(string), 64) - if err != nil { - k.Websocket.DataHandler <- err - } - stopPrice, err := strconv.ParseFloat(tradeData["stopprice"].(string), 64) - if err != nil { - k.Websocket.DataHandler <- err - } - price, err := strconv.ParseFloat(tradeData["price"].(string), 64) - if err != nil { - k.Websocket.DataHandler <- err - } - limitPrice, err := strconv.ParseFloat(tradeData["limitprice"].(string), 64) - if err != nil { - k.Websocket.DataHandler <- err - } - fee, err := strconv.ParseFloat(tradeData["fee"].(string), 64) - if err != nil { - k.Websocket.DataHandler <- err - } - descriptionSubData := tradeData["description"].(map[string]interface{}) - descriptionPrice, err := strconv.ParseFloat(descriptionSubData["price"].(string), 64) - if err != nil { - k.Websocket.DataHandler <- err - } - descriptionPrice2, err := strconv.ParseFloat(descriptionSubData["price2"].(string), 64) - if err != nil { - k.Websocket.DataHandler <- err - } - description := WsOpenOrderDescription{ - Close: descriptionSubData["close"].(string), - Leverage: descriptionSubData["leverage"].(string), - Order: descriptionSubData["order"].(string), - OrderType: descriptionSubData["ordertype"].(string), - Pair: descriptionSubData["pair"].(string), - Price: descriptionPrice, - Price2: descriptionPrice2, - Type: descriptionSubData["type"].(string), - } - - k.Websocket.DataHandler <- WsOpenOrders{ - Cost: cost, - ExpireTime: time.Unix(expireTime, expireTimeNano), - Description: description, - Fee: fee, - LimitPrice: limitPrice, - Misc: tradeData["misc"].(string), - OFlags: tradeData["oflags"].(string), - OpenTime: time.Unix(openTime, openTimeNano), - Price: price, - RefID: tradeData["refid"].(string), - StartTime: time.Unix(startTime, startTimeNano), - Status: tradeData["status"].(string), - StopPrice: stopPrice, - UserReference: userReference, - Volume: volume, - ExecutedVolume: executedVolume, + k.Websocket.DataHandler <- &order.Modify{ + Exchange: k.Name, + ID: val.OrderTransactionID, + Trades: []order.TradeHistory{trade}, } } } - } else { - k.Websocket.DataHandler <- errors.New(k.Name + " - Invalid own trades data") + return nil } + return errors.New(k.Name + " - Invalid own trades data") +} + +func (k *Kraken) wsProcessOpenOrders(ownOrders interface{}) error { + if data, ok := ownOrders.([]interface{}); ok { + for i := range data { + orders, err := json.Marshal(data[i]) + if err != nil { + return err + } + var result map[string]*WsOpenOrder + err = json.Unmarshal(orders, &result) + if err != nil { + return err + } + for key, val := range result { + var oStatus order.Status + oStatus, err = order.StringToOrderStatus(val.Status) + if err != nil { + k.Websocket.DataHandler <- order.ClassificationError{ + Exchange: k.Name, + OrderID: key, + Err: err, + } + } + if val.Description.Price > 0 { + startTime, startTimeNano, err := convert.SplitFloatDecimals(val.StartTime) + if err != nil { + return err + } + oSide, err := order.StringToOrderSide(val.Description.Type) + if err != nil { + k.Websocket.DataHandler <- order.ClassificationError{ + Exchange: k.Name, + OrderID: key, + Err: err, + } + } + if strings.Contains(val.Description.Order, "sell") { + oSide = order.Sell + } + oType, err := order.StringToOrderType(val.Description.Type) + if err != nil { + k.Websocket.DataHandler <- order.ClassificationError{ + Exchange: k.Name, + OrderID: key, + Err: err, + } + } + p := currency.NewPairFromString(val.Description.Pair) + var a asset.Item + a, err = k.GetPairAssetType(p) + if err != nil { + return err + } + k.Websocket.DataHandler <- &order.Modify{ + Leverage: val.Description.Leverage, + Price: val.Price, + Amount: val.Volume, + LimitPriceUpper: val.LimitPrice, + ExecutedAmount: val.ExecutedVolume, + RemainingAmount: val.Volume - val.ExecutedVolume, + Fee: val.Fee, + Exchange: k.Name, + ID: key, + Type: oType, + Side: oSide, + Status: oStatus, + AssetType: a, + Date: time.Unix(startTime, startTimeNano), + Pair: p, + } + } else { + k.Websocket.DataHandler <- &order.Modify{ + Exchange: k.Name, + ID: key, + Status: oStatus, + } + } + } + } + return nil + } + return errors.New(k.Name + " - Invalid own trades data") } // addNewSubscriptionChannelData stores channel ids, pairs and subscription types to an array // allowing correlation between subscriptions and returned data -func addNewSubscriptionChannelData(response *WebsocketEventResponse) { +func (k *Kraken) addNewSubscriptionChannelData(response *wsSubscription) { // We change the / to - to maintain compatibility with REST/config - pair := currency.NewPairWithDelimiter(response.Pair.Base.String(), - response.Pair.Quote.String(), "-") + var pair currency.Pair + if response.Pair != "" { + pair = currency.NewPairFromString(response.Pair) + pair.Delimiter = k.CurrencyPairs.RequestFormat.Delimiter + } subscriptionChannelPair = append(subscriptionChannelPair, WebsocketChannelData{ Subscription: response.Subscription.Name, Pair: pair, @@ -454,47 +439,34 @@ func getSubscriptionChannelData(id int64) WebsocketChannelData { } // wsProcessTickers converts ticker data and sends it to the datahandler -func (k *Kraken) wsProcessTickers(channelData *WebsocketChannelData, data map[string]interface{}) { +func (k *Kraken) wsProcessTickers(channelData *WebsocketChannelData, data map[string]interface{}) error { closePrice, err := strconv.ParseFloat(data["c"].([]interface{})[0].(string), 64) if err != nil { - k.Websocket.DataHandler <- err - return + return err } - openPrice, err := strconv.ParseFloat(data["o"].([]interface{})[0].(string), 64) if err != nil { - k.Websocket.DataHandler <- err - return + return err } - highPrice, err := strconv.ParseFloat(data["h"].([]interface{})[0].(string), 64) if err != nil { - k.Websocket.DataHandler <- err - return + return err } - lowPrice, err := strconv.ParseFloat(data["l"].([]interface{})[0].(string), 64) if err != nil { - k.Websocket.DataHandler <- err - return + return err } - quantity, err := strconv.ParseFloat(data["v"].([]interface{})[0].(string), 64) if err != nil { - k.Websocket.DataHandler <- err - return + return err } - ask, err := strconv.ParseFloat(data["a"].([]interface{})[0].(string), 64) if err != nil { - k.Websocket.DataHandler <- err - return + return err } - bid, err := strconv.ParseFloat(data["b"].([]interface{})[0].(string), 64) if err != nil { - k.Websocket.DataHandler <- err - return + return err } k.Websocket.DataHandler <- &ticker.Price{ @@ -509,9 +481,10 @@ func (k *Kraken) wsProcessTickers(channelData *WebsocketChannelData, data map[st AssetType: asset.Spot, Pair: channelData.Pair, } + return nil } -// wsProcessTickers converts ticker data and sends it to the datahandler +// wsProcessSpread converts spread/orderbook data and sends it to the datahandler func (k *Kraken) wsProcessSpread(channelData *WebsocketChannelData, data []interface{}) { bestBid := data[0].(string) bestAsk := data[1].(string) @@ -561,7 +534,10 @@ func (k *Kraken) wsProcessTrades(channelData *WebsocketChannelData, data []inter k.Websocket.DataHandler <- err return } - + var tSide = order.Buy + if trade[3].(string) == "s" { + tSide = order.Sell + } k.Websocket.DataHandler <- wshandler.TradeData{ AssetType: asset.Spot, CurrencyPair: channelData.Pair, @@ -569,17 +545,20 @@ func (k *Kraken) wsProcessTrades(channelData *WebsocketChannelData, data []inter Price: price, Amount: amount, Timestamp: timeUnix, - Side: trade[3].(string), + Side: tSide, } } } // wsProcessOrderBook determines if the orderbook data is partial or update // Then sends to appropriate fun -func (k *Kraken) wsProcessOrderBook(channelData *WebsocketChannelData, data map[string]interface{}) { +func (k *Kraken) wsProcessOrderBook(channelData *WebsocketChannelData, data map[string]interface{}) error { if fullAsk, ok := data["as"].([]interface{}); ok { fullBids := data["as"].([]interface{}) - k.wsProcessOrderBookPartial(channelData, fullAsk, fullBids) + err := k.wsProcessOrderBookPartial(channelData, fullAsk, fullBids) + if err != nil { + return err + } } else { askData, asksExist := data["a"].([]interface{}) bidData, bidsExist := data["b"].([]interface{}) @@ -593,13 +572,15 @@ func (k *Kraken) wsProcessOrderBook(channelData *WebsocketChannelData, data map[ Currency: channelData.Pair, } k.Websocket.ResubscribeToChannel(subscriptionToRemove) + return err } } } + return nil } // wsProcessOrderBookPartial creates a new orderbook entry for a given currency pair -func (k *Kraken) wsProcessOrderBookPartial(channelData *WebsocketChannelData, askData, bidData []interface{}) { +func (k *Kraken) wsProcessOrderBookPartial(channelData *WebsocketChannelData, askData, bidData []interface{}) error { base := orderbook.Base{ Pair: channelData.Pair, AssetType: asset.Spot, @@ -612,13 +593,11 @@ func (k *Kraken) wsProcessOrderBookPartial(channelData *WebsocketChannelData, as asks := askData[i].([]interface{}) price, err := strconv.ParseFloat(asks[0].(string), 64) if err != nil { - k.Websocket.DataHandler <- err - return + return err } amount, err := strconv.ParseFloat(asks[1].(string), 64) if err != nil { - k.Websocket.DataHandler <- err - return + return err } base.Asks = append(base.Asks, orderbook.Item{ Amount: amount, @@ -626,8 +605,7 @@ func (k *Kraken) wsProcessOrderBookPartial(channelData *WebsocketChannelData, as }) timeData, err := strconv.ParseFloat(asks[2].(string), 64) if err != nil { - k.Websocket.DataHandler <- err - return + return err } sec, dec := math.Modf(timeData) askUpdatedTime := time.Unix(int64(sec), int64(dec*(1e9))) @@ -640,13 +618,11 @@ func (k *Kraken) wsProcessOrderBookPartial(channelData *WebsocketChannelData, as bids := bidData[i].([]interface{}) price, err := strconv.ParseFloat(bids[0].(string), 64) if err != nil { - k.Websocket.DataHandler <- err - return + return err } amount, err := strconv.ParseFloat(bids[1].(string), 64) if err != nil { - k.Websocket.DataHandler <- err - return + return err } base.Bids = append(base.Bids, orderbook.Item{ Amount: amount, @@ -654,8 +630,7 @@ func (k *Kraken) wsProcessOrderBookPartial(channelData *WebsocketChannelData, as }) timeData, err := strconv.ParseFloat(bids[2].(string), 64) if err != nil { - k.Websocket.DataHandler <- err - return + return err } sec, dec := math.Modf(timeData) bidUpdateTime := time.Unix(int64(sec), int64(dec*(1e9))) @@ -667,14 +642,14 @@ func (k *Kraken) wsProcessOrderBookPartial(channelData *WebsocketChannelData, as base.ExchangeName = k.Name err := k.Websocket.Orderbook.LoadSnapshot(&base) if err != nil { - k.Websocket.DataHandler <- err - return + return err } k.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{ Exchange: k.Name, Asset: asset.Spot, Pair: channelData.Pair, } + return nil } // wsProcessOrderBookUpdate updates an orderbook entry for a given currency pair @@ -757,51 +732,44 @@ func (k *Kraken) wsProcessOrderBookUpdate(channelData *WebsocketChannelData, ask } // wsProcessCandles converts candle data and sends it to the data handler -func (k *Kraken) wsProcessCandles(channelData *WebsocketChannelData, data []interface{}) { +func (k *Kraken) wsProcessCandles(channelData *WebsocketChannelData, data []interface{}) error { startTime, err := strconv.ParseFloat(data[0].(string), 64) if err != nil { - k.Websocket.DataHandler <- err - return + return err } 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 + return err } 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 + return err } highPrice, err := strconv.ParseFloat(data[3].(string), 64) if err != nil { - k.Websocket.DataHandler <- err - return + return err } lowPrice, err := strconv.ParseFloat(data[4].(string), 64) if err != nil { - k.Websocket.DataHandler <- err - return + return err } closePrice, err := strconv.ParseFloat(data[5].(string), 64) if err != nil { - k.Websocket.DataHandler <- err - return + return err } volume, err := strconv.ParseFloat(data[7].(string), 64) if err != nil { - k.Websocket.DataHandler <- err - return + return err } k.Websocket.DataHandler <- wshandler.KlineData{ @@ -819,6 +787,7 @@ func (k *Kraken) wsProcessCandles(channelData *WebsocketChannelData, data []inte ClosePrice: closePrice, Volume: volume, } + return nil } // GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions() diff --git a/exchanges/kraken/kraken_wrapper.go b/exchanges/kraken/kraken_wrapper.go index 2c6c6239..43602df7 100644 --- a/exchanges/kraken/kraken_wrapper.go +++ b/exchanges/kraken/kraken_wrapper.go @@ -111,6 +111,8 @@ func (k *Kraken) SetDefaults() { SubmitOrder: true, CancelOrder: true, CancelOrders: true, + GetOrders: true, + GetOrder: true, }, WithdrawPermissions: exchange.AutoWithdrawCryptoWithSetup | exchange.WithdrawCryptoWith2FA | @@ -436,8 +438,8 @@ func (k *Kraken) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) { if k.Websocket.CanUseAuthenticatedWebsocketForWrapper() { var resp string resp, err = k.wsAddOrder(&WsAddOrderRequest{ - OrderType: s.OrderType.String(), - OrderSide: s.OrderSide.String(), + OrderType: s.Type.String(), + OrderSide: s.Side.String(), Pair: s.Pair.String(), Price: s.Price, Volume: s.Amount, @@ -450,8 +452,8 @@ func (k *Kraken) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) { } else { var response AddOrderResponse response, err = k.AddOrder(s.Pair.String(), - s.OrderSide.String(), - s.OrderType.String(), + s.Side.String(), + s.Type.String(), s.Amount, s.Price, 0, @@ -464,7 +466,7 @@ func (k *Kraken) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) { submitOrderResponse.OrderID = strings.Join(response.TransactionIds, ", ") } } - if s.OrderType == order.Market { + if s.Type == order.Market { submitOrderResponse.FullyMatched = true } submitOrderResponse.IsOrderPlaced = true @@ -480,9 +482,9 @@ func (k *Kraken) ModifyOrder(action *order.Modify) (string, error) { // CancelOrder cancels an order by its corresponding ID number func (k *Kraken) CancelOrder(order *order.Cancel) error { if k.Websocket.CanUseAuthenticatedWebsocketForWrapper() { - return k.wsCancelOrders([]string{order.OrderID}) + return k.wsCancelOrders([]string{order.ID}) } - _, err := k.CancelExistingOrder(order.OrderID) + _, err := k.CancelExistingOrder(order.ID) return err } @@ -547,10 +549,10 @@ func (k *Kraken) GetOrderInfo(orderID string) (order.Detail, error) { orderDetail = order.Detail{ Exchange: k.Name, ID: orderID, - CurrencyPair: currency.NewPairFromString(orderInfo.Description.Pair), - OrderSide: side, - OrderType: oType, - OrderDate: time.Unix(firstNum, decNum), + Pair: currency.NewPairFromString(orderInfo.Description.Pair), + Side: side, + Type: oType, + Date: time.Unix(firstNum, decNum), Status: status, Price: orderInfo.Price, Amount: orderInfo.Volume, @@ -655,17 +657,17 @@ func (k *Kraken) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, e RemainingAmount: (resp.Open[i].Volume - resp.Open[i].VolumeExecuted), ExecutedAmount: resp.Open[i].VolumeExecuted, Exchange: k.Name, - OrderDate: orderDate, + Date: orderDate, Price: resp.Open[i].Description.Price, - OrderSide: side, - OrderType: orderType, - CurrencyPair: symbol, + Side: side, + Type: orderType, + Pair: symbol, }) } order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) - order.FilterOrdersBySide(&orders, req.OrderSide) - order.FilterOrdersByCurrencies(&orders, req.Currencies) + order.FilterOrdersBySide(&orders, req.Side) + order.FilterOrdersByCurrencies(&orders, req.Pairs) return orders, nil } @@ -698,16 +700,16 @@ func (k *Kraken) GetOrderHistory(getOrdersRequest *order.GetOrdersRequest) ([]or RemainingAmount: (resp.Closed[i].Volume - resp.Closed[i].VolumeExecuted), ExecutedAmount: resp.Closed[i].VolumeExecuted, Exchange: k.Name, - OrderDate: orderDate, + Date: orderDate, Price: resp.Closed[i].Description.Price, - OrderSide: side, - OrderType: orderType, - CurrencyPair: symbol, + Side: side, + Type: orderType, + Pair: symbol, }) } - order.FilterOrdersBySide(&orders, getOrdersRequest.OrderSide) - order.FilterOrdersByCurrencies(&orders, getOrdersRequest.Currencies) + order.FilterOrdersBySide(&orders, getOrdersRequest.Side) + order.FilterOrdersByCurrencies(&orders, getOrdersRequest.Pairs) return orders, nil } diff --git a/exchanges/lakebtc/lakebtc_test.go b/exchanges/lakebtc/lakebtc_test.go index 296fab7a..9a83ff7d 100644 --- a/exchanges/lakebtc/lakebtc_test.go +++ b/exchanges/lakebtc/lakebtc_test.go @@ -263,7 +263,7 @@ func TestFormatWithdrawPermissions(t *testing.T) { func TestGetActiveOrders(t *testing.T) { var getOrdersRequest = order.GetOrdersRequest{ - OrderType: order.AnyType, + Type: order.AnyType, } _, err := l.GetActiveOrders(&getOrdersRequest) @@ -276,7 +276,7 @@ func TestGetActiveOrders(t *testing.T) { func TestGetOrderHistory(t *testing.T) { var getOrdersRequest = order.GetOrdersRequest{ - OrderType: order.AnyType, + Type: order.AnyType, } _, err := l.GetOrderHistory(&getOrdersRequest) @@ -303,11 +303,11 @@ func TestSubmitOrder(t *testing.T) { Base: currency.BTC, Quote: currency.EUR, }, - OrderSide: order.Buy, - OrderType: order.Limit, - Price: 1, - Amount: 1, - ClientID: "meowOrder", + Side: order.Buy, + Type: order.Limit, + Price: 1, + Amount: 1, + ClientID: "meowOrder", } response, err := l.SubmitOrder(orderSubmission) if areTestAPIKeysSet() && (err != nil || !response.IsOrderPlaced) { @@ -324,10 +324,10 @@ func TestCancelExchangeOrder(t *testing.T) { currencyPair := currency.NewPair(currency.LTC, currency.BTC) var orderCancellation = &order.Cancel{ - OrderID: "1", + ID: "1", WalletAddress: core.BitcoinDonationAddress, AccountID: "1", - CurrencyPair: currencyPair, + Pair: currencyPair, } err := l.CancelOrder(orderCancellation) @@ -346,10 +346,10 @@ func TestCancelAllExchangeOrders(t *testing.T) { currencyPair := currency.NewPair(currency.LTC, currency.BTC) var orderCancellation = &order.Cancel{ - OrderID: "1", + ID: "1", WalletAddress: core.BitcoinDonationAddress, AccountID: "1", - CurrencyPair: currencyPair, + Pair: currencyPair, } resp, err := l.CancelAllOrders(orderCancellation) diff --git a/exchanges/lakebtc/lakebtc_websocket.go b/exchanges/lakebtc/lakebtc_websocket.go index 5c222f79..7af6622f 100644 --- a/exchanges/lakebtc/lakebtc_websocket.go +++ b/exchanges/lakebtc/lakebtc_websocket.go @@ -147,15 +147,22 @@ func (l *LakeBTC) processTrades(data, channel string) error { } curr := l.getCurrencyFromChannel(channel) for i := range tradeData.Trades { + tSide, err := order.StringToOrderSide(tradeData.Trades[i].Type) + if err != nil { + l.Websocket.DataHandler <- order.ClassificationError{ + Exchange: l.Name, + Err: err, + } + } l.Websocket.DataHandler <- wshandler.TradeData{ Timestamp: time.Unix(tradeData.Trades[i].Date, 0), CurrencyPair: curr, AssetType: asset.Spot, Exchange: l.Name, - EventType: asset.Spot.String(), + EventType: order.UnknownType, Price: tradeData.Trades[i].Price, Amount: tradeData.Trades[i].Amount, - Side: tradeData.Trades[i].Type, + Side: tSide, } } return nil diff --git a/exchanges/lakebtc/lakebtc_wrapper.go b/exchanges/lakebtc/lakebtc_wrapper.go index dc18a7f7..e81aba47 100644 --- a/exchanges/lakebtc/lakebtc_wrapper.go +++ b/exchanges/lakebtc/lakebtc_wrapper.go @@ -347,7 +347,7 @@ func (l *LakeBTC) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) { return submitOrderResponse, err } - isBuyOrder := s.OrderSide == order.Buy + isBuyOrder := s.Side == order.Buy response, err := l.Trade(isBuyOrder, s.Amount, s.Price, s.Pair.Lower().String()) if err != nil { @@ -358,7 +358,7 @@ func (l *LakeBTC) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) { } submitOrderResponse.IsOrderPlaced = true - if s.OrderType == order.Market { + if s.Type == order.Market { submitOrderResponse.FullyMatched = true } return submitOrderResponse, nil @@ -372,7 +372,7 @@ func (l *LakeBTC) ModifyOrder(action *order.Modify) (string, error) { // CancelOrder cancels an order by its corresponding ID number func (l *LakeBTC) CancelOrder(order *order.Cancel) error { - orderIDInt, err := strconv.ParseInt(order.OrderID, 10, 64) + orderIDInt, err := strconv.ParseInt(order.ID, 10, 64) if err != nil { return err @@ -476,19 +476,19 @@ func (l *LakeBTC) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, side := order.Side(strings.ToUpper(resp[i].Type)) orders = append(orders, order.Detail{ - Amount: resp[i].Amount, - ID: strconv.FormatInt(resp[i].ID, 10), - Price: resp[i].Price, - OrderSide: side, - OrderDate: orderDate, - CurrencyPair: symbol, - Exchange: l.Name, + Amount: resp[i].Amount, + ID: strconv.FormatInt(resp[i].ID, 10), + Price: resp[i].Price, + Side: side, + Date: orderDate, + Pair: symbol, + Exchange: l.Name, }) } order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) - order.FilterOrdersBySide(&orders, req.OrderSide) - order.FilterOrdersByCurrencies(&orders, req.Currencies) + order.FilterOrdersBySide(&orders, req.Side) + order.FilterOrdersByCurrencies(&orders, req.Pairs) return orders, nil } @@ -513,19 +513,19 @@ func (l *LakeBTC) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail, side := order.Side(strings.ToUpper(resp[i].Type)) orders = append(orders, order.Detail{ - Amount: resp[i].Amount, - ID: strconv.FormatInt(resp[i].ID, 10), - Price: resp[i].Price, - OrderSide: side, - OrderDate: orderDate, - CurrencyPair: symbol, - Exchange: l.Name, + Amount: resp[i].Amount, + ID: strconv.FormatInt(resp[i].ID, 10), + Price: resp[i].Price, + Side: side, + Date: orderDate, + Pair: symbol, + Exchange: l.Name, }) } order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) - order.FilterOrdersBySide(&orders, req.OrderSide) - order.FilterOrdersByCurrencies(&orders, req.Currencies) + order.FilterOrdersBySide(&orders, req.Side) + order.FilterOrdersByCurrencies(&orders, req.Pairs) return orders, nil } diff --git a/exchanges/lbank/lbank_test.go b/exchanges/lbank/lbank_test.go index 7eacaba9..a7db8c2e 100644 --- a/exchanges/lbank/lbank_test.go +++ b/exchanges/lbank/lbank_test.go @@ -316,11 +316,11 @@ func TestSubmitOrder(t *testing.T) { Quote: currency.USDT, Delimiter: "_", }, - OrderSide: order.Buy, - OrderType: order.Limit, - Price: 1, - Amount: 1, - ClientID: "meowOrder", + Side: order.Buy, + Type: order.Limit, + Price: 1, + Amount: 1, + ClientID: "meowOrder", } response, err := l.SubmitOrder(orderSubmission) if areTestAPIKeysSet() && (err != nil || !response.IsOrderPlaced) { @@ -337,8 +337,8 @@ func TestCancelOrder(t *testing.T) { } cp := currency.NewPairWithDelimiter(currency.ETH.String(), currency.BTC.String(), "_") var a order.Cancel - a.CurrencyPair = cp - a.OrderID = "24f7ce27-af1d-4dca-a8c1-ef1cbeec1b23" + a.Pair = cp + a.ID = "24f7ce27-af1d-4dca-a8c1-ef1cbeec1b23" err := l.CancelOrder(&a) if err != nil { t.Error(err) @@ -400,7 +400,7 @@ func TestGetOrderHistory(t *testing.T) { t.Skip("API keys required but not set, skipping test") } var input order.GetOrdersRequest - input.OrderSide = order.Buy + input.Side = order.Buy _, err := l.GetOrderHistory(&input) if err != nil { t.Error(err) diff --git a/exchanges/lbank/lbank_wrapper.go b/exchanges/lbank/lbank_wrapper.go index bc1926bb..3842ffa8 100644 --- a/exchanges/lbank/lbank_wrapper.go +++ b/exchanges/lbank/lbank_wrapper.go @@ -316,14 +316,14 @@ func (l *Lbank) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) { return resp, err } - if s.OrderSide != order.Buy && s.OrderSide != order.Sell { + if s.Side != order.Buy && s.Side != order.Sell { return resp, fmt.Errorf("%s order side is not supported by the exchange", - s.OrderSide) + s.Side) } tempResp, err := l.CreateOrder( l.FormatExchangeCurrency(s.Pair, asset.Spot).String(), - s.OrderSide.String(), + s.Side.String(), s.Amount, s.Price) if err != nil { @@ -331,7 +331,7 @@ func (l *Lbank) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) { } resp.IsOrderPlaced = true resp.OrderID = tempResp.OrderID - if s.OrderType == order.Market { + if s.Type == order.Market { resp.FullyMatched = true } return resp, nil @@ -345,8 +345,8 @@ func (l *Lbank) ModifyOrder(action *order.Modify) (string, error) { // CancelOrder cancels an order by its corresponding ID number func (l *Lbank) CancelOrder(order *order.Cancel) error { - _, err := l.RemoveOrder(l.FormatExchangeCurrency(order.CurrencyPair, - order.AssetType).String(), order.OrderID) + _, err := l.RemoveOrder(l.FormatExchangeCurrency(order.Pair, + order.AssetType).String(), order.ID) return err } @@ -359,7 +359,7 @@ func (l *Lbank) CancelAllOrders(orders *order.Cancel) (order.CancelAllResponse, } for key := range orderIDs { - if key != orders.CurrencyPair.String() { + if key != orders.Pair.String() { continue } var x, y = 0, 0 @@ -425,11 +425,11 @@ func (l *Lbank) GetOrderInfo(orderID string) (order.Detail, error) { return resp, err } resp.Exchange = l.Name - resp.CurrencyPair = currency.NewPairFromString(key) + resp.Pair = currency.NewPairFromString(key) if strings.EqualFold(tempResp.Orders[0].Type, order.Buy.String()) { - resp.OrderSide = order.Buy + resp.Side = order.Buy } else { - resp.OrderSide = order.Sell + resp.Side = order.Sell } z := tempResp.Orders[0].Status switch { @@ -514,11 +514,11 @@ func (l *Lbank) GetActiveOrders(getOrdersRequest *order.GetOrdersRequest) ([]ord return finalResp, err } resp.Exchange = l.Name - resp.CurrencyPair = currency.NewPairFromString(key) + resp.Pair = currency.NewPairFromString(key) if strings.EqualFold(tempResp.Orders[0].Type, order.Buy.String()) { - resp.OrderSide = order.Buy + resp.Side = order.Buy } else { - resp.OrderSide = order.Sell + resp.Side = order.Sell } z := tempResp.Orders[0].Status switch { @@ -537,7 +537,7 @@ func (l *Lbank) GetActiveOrders(getOrdersRequest *order.GetOrdersRequest) ([]ord } resp.Price = tempResp.Orders[0].Price resp.Amount = tempResp.Orders[0].Amount - resp.OrderDate = time.Unix(tempResp.Orders[0].CreateTime, 9) + resp.Date = time.Unix(tempResp.Orders[0].CreateTime, 9) resp.ExecutedAmount = tempResp.Orders[0].DealAmount resp.RemainingAmount = tempResp.Orders[0].Amount - tempResp.Orders[0].DealAmount resp.Fee, err = l.GetFeeByType(&exchange.FeeBuilder{ @@ -547,15 +547,15 @@ func (l *Lbank) GetActiveOrders(getOrdersRequest *order.GetOrdersRequest) ([]ord if err != nil { resp.Fee = lbankFeeNotFound } - for y := int(0); y < len(getOrdersRequest.Currencies); y++ { - if getOrdersRequest.Currencies[y].String() != key { + for y := int(0); y < len(getOrdersRequest.Pairs); y++ { + if getOrdersRequest.Pairs[y].String() != key { continue } - if getOrdersRequest.OrderSide == "ANY" { + if getOrdersRequest.Side == "ANY" { finalResp = append(finalResp, resp) continue } - if strings.EqualFold(getOrdersRequest.OrderSide.String(), + if strings.EqualFold(getOrdersRequest.Side.String(), tempResp.Orders[0].Type) { finalResp = append(finalResp, resp) } @@ -571,10 +571,10 @@ func (l *Lbank) GetOrderHistory(getOrdersRequest *order.GetOrdersRequest) ([]ord var finalResp []order.Detail var resp order.Detail var tempCurr currency.Pairs - if len(getOrdersRequest.Currencies) == 0 { + if len(getOrdersRequest.Pairs) == 0 { tempCurr = l.GetEnabledPairs(asset.Spot) } else { - tempCurr = getOrdersRequest.Currencies + tempCurr = getOrdersRequest.Pairs } for a := range tempCurr { p := l.FormatExchangeCurrency(tempCurr[a], asset.Spot).String() @@ -590,11 +590,11 @@ func (l *Lbank) GetOrderHistory(getOrdersRequest *order.GetOrdersRequest) ([]ord } for x := 0; x < len(tempResp.Orders); x++ { resp.Exchange = l.Name - resp.CurrencyPair = currency.NewPairFromString(tempResp.Orders[x].Symbol) + resp.Pair = currency.NewPairFromString(tempResp.Orders[x].Symbol) if strings.EqualFold(tempResp.Orders[x].Type, order.Buy.String()) { - resp.OrderSide = order.Buy + resp.Side = order.Buy } else { - resp.OrderSide = order.Sell + resp.Side = order.Sell } z := tempResp.Orders[x].Status switch { @@ -613,7 +613,7 @@ func (l *Lbank) GetOrderHistory(getOrdersRequest *order.GetOrdersRequest) ([]ord } resp.Price = tempResp.Orders[x].Price resp.Amount = tempResp.Orders[x].Amount - resp.OrderDate = time.Unix(tempResp.Orders[x].CreateTime, 9) + resp.Date = time.Unix(tempResp.Orders[x].CreateTime, 9) resp.ExecutedAmount = tempResp.Orders[x].DealAmount resp.RemainingAmount = tempResp.Orders[x].Price - tempResp.Orders[x].DealAmount resp.Fee, err = l.GetFeeByType(&exchange.FeeBuilder{ diff --git a/exchanges/localbitcoins/localbitcoins_test.go b/exchanges/localbitcoins/localbitcoins_test.go index b49fdad1..b71dc5a3 100644 --- a/exchanges/localbitcoins/localbitcoins_test.go +++ b/exchanges/localbitcoins/localbitcoins_test.go @@ -203,7 +203,7 @@ func TestGetActiveOrders(t *testing.T) { t.Parallel() var getOrdersRequest = order.GetOrdersRequest{ - OrderType: order.AnyType, + Type: order.AnyType, } _, err := l.GetActiveOrders(&getOrdersRequest) @@ -221,7 +221,7 @@ func TestGetOrderHistory(t *testing.T) { t.Parallel() var getOrdersRequest = order.GetOrdersRequest{ - OrderType: order.AnyType, + Type: order.AnyType, } _, err := l.GetOrderHistory(&getOrdersRequest) @@ -253,11 +253,11 @@ func TestSubmitOrder(t *testing.T) { Base: currency.BTC, Quote: currency.EUR, }, - OrderSide: order.Buy, - OrderType: order.Limit, - Price: 1, - Amount: 1, - ClientID: "meowOrder", + Side: order.Buy, + Type: order.Limit, + Price: 1, + Amount: 1, + ClientID: "meowOrder", } response, err := l.SubmitOrder(orderSubmission) switch { @@ -277,10 +277,10 @@ func TestCancelExchangeOrder(t *testing.T) { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } var orderCancellation = &order.Cancel{ - OrderID: "1", + ID: "1", WalletAddress: core.BitcoinDonationAddress, AccountID: "1", - CurrencyPair: currency.NewPair(currency.LTC, currency.BTC), + Pair: currency.NewPair(currency.LTC, currency.BTC), } err := l.CancelOrder(orderCancellation) @@ -301,10 +301,10 @@ func TestCancelAllExchangeOrders(t *testing.T) { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } var orderCancellation = &order.Cancel{ - OrderID: "1", + ID: "1", WalletAddress: core.BitcoinDonationAddress, AccountID: "1", - CurrencyPair: currency.NewPair(currency.LTC, currency.BTC), + Pair: currency.NewPair(currency.LTC, currency.BTC), } resp, err := l.CancelAllOrders(orderCancellation) diff --git a/exchanges/localbitcoins/localbitcoins_wrapper.go b/exchanges/localbitcoins/localbitcoins_wrapper.go index a6418c57..af482698 100644 --- a/exchanges/localbitcoins/localbitcoins_wrapper.go +++ b/exchanges/localbitcoins/localbitcoins_wrapper.go @@ -309,7 +309,7 @@ func (l *LocalBitcoins) SubmitOrder(s *order.Submit) (order.SubmitResponse, erro Currency: s.Pair.Quote.String(), AccountInfo: "-", BankName: "Bank", - MSG: s.OrderSide.String(), + MSG: s.Side.String(), SMSVerficationRequired: true, TrackMaxAmount: true, RequireTrustedByAdvertiser: true, @@ -368,7 +368,7 @@ func (l *LocalBitcoins) ModifyOrder(action *order.Modify) (string, error) { // CancelOrder cancels an order by its corresponding ID number func (l *LocalBitcoins) CancelOrder(order *order.Cancel) error { - return l.DeleteAd(order.OrderID) + return l.DeleteAd(order.ID) } // CancelAllOrders cancels all orders associated with a currency pair @@ -472,13 +472,13 @@ func (l *LocalBitcoins) GetActiveOrders(getOrdersRequest *order.GetOrdersRequest } orders = append(orders, order.Detail{ - Amount: resp[i].Data.AmountBTC, - Price: resp[i].Data.Amount, - ID: strconv.FormatInt(int64(resp[i].Data.Advertisement.ID), 10), - OrderDate: orderDate, - Fee: resp[i].Data.FeeBTC, - OrderSide: side, - CurrencyPair: currency.NewPairWithDelimiter(currency.BTC.String(), + Amount: resp[i].Data.AmountBTC, + Price: resp[i].Data.Amount, + ID: strconv.FormatInt(int64(resp[i].Data.Advertisement.ID), 10), + Date: orderDate, + Fee: resp[i].Data.FeeBTC, + Side: side, + Pair: currency.NewPairWithDelimiter(currency.BTC.String(), resp[i].Data.Currency, l.GetPairFormat(asset.Spot, false).Delimiter), Exchange: l.Name, @@ -487,7 +487,7 @@ func (l *LocalBitcoins) GetActiveOrders(getOrdersRequest *order.GetOrdersRequest order.FilterOrdersByTickRange(&orders, getOrdersRequest.StartTicks, getOrdersRequest.EndTicks) - order.FilterOrdersBySide(&orders, getOrdersRequest.OrderSide) + order.FilterOrdersBySide(&orders, getOrdersRequest.Side) return orders, nil } @@ -548,14 +548,14 @@ func (l *LocalBitcoins) GetOrderHistory(getOrdersRequest *order.GetOrdersRequest } orders = append(orders, order.Detail{ - Amount: allTrades[i].Data.AmountBTC, - Price: allTrades[i].Data.Amount, - ID: strconv.FormatInt(int64(allTrades[i].Data.Advertisement.ID), 10), - OrderDate: orderDate, - Fee: allTrades[i].Data.FeeBTC, - OrderSide: side, - Status: order.Status(status), - CurrencyPair: currency.NewPairWithDelimiter(currency.BTC.String(), + Amount: allTrades[i].Data.AmountBTC, + Price: allTrades[i].Data.Amount, + ID: strconv.FormatInt(int64(allTrades[i].Data.Advertisement.ID), 10), + Date: orderDate, + Fee: allTrades[i].Data.FeeBTC, + Side: side, + Status: order.Status(status), + Pair: currency.NewPairWithDelimiter(currency.BTC.String(), allTrades[i].Data.Currency, l.GetPairFormat(asset.Spot, false).Delimiter), Exchange: l.Name, @@ -564,7 +564,7 @@ func (l *LocalBitcoins) GetOrderHistory(getOrdersRequest *order.GetOrdersRequest order.FilterOrdersByTickRange(&orders, getOrdersRequest.StartTicks, getOrdersRequest.EndTicks) - order.FilterOrdersBySide(&orders, getOrdersRequest.OrderSide) + order.FilterOrdersBySide(&orders, getOrdersRequest.Side) return orders, nil } diff --git a/exchanges/mock/server.go b/exchanges/mock/server.go index 3965ffec..31c8d7f6 100644 --- a/exchanges/mock/server.go +++ b/exchanges/mock/server.go @@ -123,7 +123,7 @@ func RegisterHandler(pattern string, mock map[string][]HTTPResponse, mux *http.S MessageWriteJSON(w, http.StatusOK, payload) return - case http.MethodPost: + case http.MethodPost, http.MethodPut: switch r.Header.Get(contentType) { case applicationURLEncoded: readBody, err := ioutil.ReadAll(r.Body) diff --git a/exchanges/okcoin/okcoin_test.go b/exchanges/okcoin/okcoin_test.go index 9365a85a..7e591909 100644 --- a/exchanges/okcoin/okcoin_test.go +++ b/exchanges/okcoin/okcoin_test.go @@ -767,7 +767,7 @@ func TestSendWsMessages(t *testing.T) { } wg := sync.WaitGroup{} wg.Add(1) - go o.WsHandleData(&wg) + go o.WsReadData(&wg) wg.Wait() subscription := wshandler.WebsocketChannelSubscription{ @@ -827,23 +827,12 @@ func TestOrderBookUpdateChecksumCalculator(t *testing.T) { } original := `{"table":"spot/depth","action":"partial","data":[{"instrument_id":"BTC-USDT","asks":[["3864.6786","0.145",1],["3864.7682","0.005",1],["3864.9851","0.57",1],["3864.9852","0.30137754",1],["3864.9986","2.81818419",1],["3864.9995","0.002",1],["3865","0.0597",1],["3865.0309","0.4",1],["3865.1995","0.004",1],["3865.3995","0.004",1],["3865.5995","0.004",1],["3865.7995","0.004",1],["3865.9995","0.004",1],["3866.0961","0.25865886",1],["3866.1995","0.004",1],["3866.3995","0.004",1],["3866.4004","0.3243",2],["3866.5995","0.004",1],["3866.7633","0.44247086",1],["3866.7995","0.004",1],["3866.9197","0.511",1],["3867.256","0.51716256",1],["3867.3951","0.02588112",1],["3867.4014","0.025",1],["3867.4566","0.02499999",1],["3867.4675","4.01155057",5],["3867.5515","1.1",1],["3867.6113","0.009",1],["3867.7349","0.026",1],["3867.7781","0.03738652",1],["3867.9163","0.0521",1],["3868.0381","0.34354941",1],["3868.0436","0.051",1],["3868.0657","0.90552172",3],["3868.1819","0.03863346",1],["3868.2013","0.194",1],["3868.346","0.051",1],["3868.3863","0.01155",1],["3868.7716","0.009",1],["3868.947","0.025",1],["3868.98","0.001",1],["3869.0764","1.03487931",1],["3869.2773","0.07724578",1],["3869.4039","0.025",1],["3869.4068","1.03",1],["3869.7068","2.06976398",1],["3870","0.5",1],["3870.0465","0.01",1],["3870.7042","0.02099651",1],["3870.9451","2.07047375",1],["3871.5254","1.2",1],["3871.5596","0.001",1],["3871.6605","0.01035032",1],["3871.7179","2.07047375",1],["3871.8816","0.51751625",1],["3872.1","0.75",1],["3872.2464","0.0646",1],["3872.3747","0.283",1],["3872.4039","0.2",1],["3872.7655","0.23179307",1],["3872.8005","2.06976398",1],["3873.1509","2",1],["3873.3215","0.26",1],["3874.1392","0.001",1],["3874.1487","3.88224364",4],["3874.1685","1.8",1],["3874.5571","0.08974762",1],["3874.734","2.06976398",1],["3874.99","0.3",1],["3875","1.001",2],["3875.0041","1.03505051",1],["3875.45","0.3",1],["3875.4766","0.15",1],["3875.7057","0.51751625",1],["3876","0.001",1],["3876.68","0.3",1],["3876.7188","0.001",1],["3877","0.75",1],["3877.31","0.035",1],["3877.38","0.3",1],["3877.7","0.3",1],["3877.88","0.3",1],["3878.0364","0.34770122",1],["3878.4525","0.48579748",1],["3878.4955","0.02812511",1],["3878.8855","0.00258579",1],["3878.9605","0.895",1],["3879","0.001",1],["3879.2984","0.002",2],["3879.432","0.001",1],["3879.6313","6",1],["3879.9999","0.002",2],["3880","1.25132834",5],["3880.2526","0.04075162",1],["3880.7145","0.0647",1],["3881.2469","1.883",1],["3881.878","0.002",2],["3884.4576","0.002",2],["3885","0.002",2],["3885.2233","0.28304103",1],["3885.7416","18",1],["3886","0.001",1],["3886.1554","5.4",1],["3887","0.001",1],["3887.0372","0.002",2],["3887.2559","0.05214011",1],["3887.9238","0.0019",1],["3888","0.15810538",4],["3889","0.001",1],["3889.5175","0.50510653",1],["3889.6168","0.002",2],["3889.9999","0.001",1],["3890","2.34968109",4],["3890.5222","0.00257806",1],["3891.2659","5",1],["3891.9999","0.00893897",1],["3892.1964","0.002",2],["3892.4358","0.0176",1],["3893.1388","1.4279",1],["3894","0.0026321",1],["3894.776","0.001",1],["3895","1.501",2],["3895.379","0.25881288",1],["3897","0.05",1],["3897.3556","0.001",1],["3897.8432","0.73708079",1],["3898","3.31353018",7],["3898.4462","4.757",1],["3898.6","0.47159638",1],["3898.8769","0.0129",1],["3899","6",2],["3899.6516","0.025",1],["3899.9352","0.001",1],["3899.9999","0.013",2],["3900","22.37447743",24],["3900.9999","0.07763916",1],["3901","0.10192487",1],["3902.1937","0.00257034",1],["3902.3991","1.5532141",1],["3902.5148","0.001",1],["3904","1.49331984",1],["3904.9999","0.95905447",1],["3905","0.501",2],["3905.0944","0.001",1],["3905.61","0.099",1],["3905.6801","0.54343686",1],["3906.2901","0.0258",1],["3907.674","0.001",1],["3907.85","1.35778084",1],["3908","0.03846153",1],["3908.23","1.95189531",1],["3908.906","0.03148978",1],["3909","0.001",1],["3909.9999","0.01398721",2],["3910","0.016",2],["3910.2536","0.001",1],["3912.5406","0.88270517",1],["3912.8332","0.001",1],["3913","1.2640608",1],["3913.87","1.69114184",1],["3913.9003","0.00256266",1],["3914","1.21766411",1],["3915","0.001",1],["3915.4128","0.001",1],["3915.7425","6.848",1],["3916","0.0050949",1],["3917.36","1.28658296",1],["3917.9924","0.001",1],["3919","0.001",1],["3919.9999","0.001",1],["3920","1.21171832",3],["3920.0002","0.20217038",1],["3920.572","0.001",1],["3921","0.128",1],["3923.0756","0.00148064",1],["3923.1516","0.001",1],["3923.86","1.38831714",1],["3925","0.01867801",2],["3925.642","0.00255499",1],["3925.7312","0.001",1],["3926","0.04290757",1],["3927","0.023",1],["3927.3175","0.01212865",1],["3927.65","1.51375612",1],["3928","0.5",1],["3928.3108","0.001",1],["3929","0.001",1],["3929.9999","0.01519338",2],["3930","0.0174985",3],["3930.21","1.49335799",1],["3930.8904","0.001",1],["3932.2999","0.01953",1],["3932.8962","7.96",1],["3933.0387","11.808",1],["3933.47","0.001",1],["3934","1.40839932",1],["3935","0.001",1],["3936.8","0.62879518",1],["3937.23","1.56977841",1],["3937.4189","0.00254735",1]],"bids":[["3864.5217","0.00540709",1],["3864.5216","0.14068758",2],["3864.2275","0.01033576",1],["3864.0989","0.00825047",1],["3864.0273","0.38",1],["3864.0272","0.4",1],["3863.9957","0.01083539",1],["3863.9184","0.01653723",1],["3863.8282","0.25588165",1],["3863.8153","0.154",1],["3863.7791","1.14122492",1],["3863.6866","0.01733662",1],["3863.6093","0.02645958",1],["3863.3775","0.02773862",1],["3863.0297","0.513",1],["3863.0286","1.1028564",2],["3862.8489","0.01",1],["3862.5972","0.01890179",1],["3862.3431","0.01152944",1],["3862.313","0.009",1],["3862.2445","0.90551002",3],["3862.0734","0.014",1],["3862.0539","0.64976067",1],["3861.8586","0.025",1],["3861.7888","0.025",1],["3861.7673","0.008",1],["3861.5785","0.01",1],["3861.3895","0.005",1],["3861.3338","0.25875855",1],["3861.161","0.01",1],["3861.1111","0.03863352",1],["3861.0732","0.51703882",1],["3860.9116","0.17754895",1],["3860.75","0.19",1],["3860.6554","0.015",1],["3860.6172","0.005",1],["3860.6088","0.008",1],["3860.4724","0.12940042",1],["3860.4424","0.25880084",1],["3860.42","0.01",1],["3860.3725","0.51760102",1],["3859.8449","0.005",1],["3859.8285","0.03738652",1],["3859.7638","0.07726703",1],["3859.4502","0.008",1],["3859.3772","0.05173471",1],["3859.3409","0.194",1],["3859","5",1],["3858.827","0.0521",1],["3858.8208","0.001",1],["3858.679","0.26",1],["3858.4814","0.07477305",1],["3858.1669","1.03503422",1],["3857.6005","0.006",1],["3857.4005","0.004",1],["3857.2005","0.004",1],["3857.1871","1.218",1],["3857.0005","0.004",1],["3856.8135","0.0646",1],["3856.8005","0.004",1],["3856.2412","0.001",1],["3856.2349","1.03503422",1],["3856.0197","0.01037339",1],["3855.8781","0.23178117",1],["3855.8005","0.004",1],["3855.7165","0.00259355",1],["3855.4858","0.25875855",1],["3854.4584","0.01",1],["3853.6616","0.001",1],["3853.1373","0.92",1],["3852.5072","0.48599702",1],["3851.3926","0.13008333",1],["3851.082","0.001",1],["3850.9317","2",1],["3850.6359","0.34770165",1],["3850.2058","0.51751624",1],["3850.0823","0.15",1],["3850.0042","0.5175171",1],["3850","0.001",1],["3849.6325","1.8",1],["3849.41","0.3",1],["3848.9686","1.85",1],["3848.7426","0.18511466",1],["3848.52","0.3",1],["3848.5024","0.001",1],["3848.42","0.3",1],["3848.1618","2.204",1],["3847.77","0.3",1],["3847.48","0.3",1],["3847.3581","2.05",1],["3846.8259","0.0646",1],["3846.59","0.3",1],["3846.49","0.3",1],["3845.9228","0.001",1],["3844.184","0.00260133",1],["3844.0092","6.3",1],["3843.3432","0.001",1],["3841","0.06300963",1],["3840.7636","0.001",1],["3840","0.201",3],["3839.7681","18",1],["3839.5328","0.05214011",1],["3838.184","0.001",1],["3837.2344","0.27589557",1],["3836.6479","5.2",1],["3836","2.37196773",3],["3835.6044","0.001",1],["3833.6053","0.25873556",1],["3833.0248","0.001",1],["3833","0.8726502",1],["3832.6859","0.00260913",1],["3832","0.007",1],["3831.637","6",1],["3831.0602","0.001",1],["3830.4452","0.001",1],["3830","0.20375718",4],["3829.7125","0.07833486",1],["3829.6283","0.3519681",1],["3829","0.0039261",1],["3827.8656","0.001",1],["3826.0001","0.53251232",1],["3826","0.0509",1],["3825.7834","0.00698562",1],["3825.286","0.001",1],["3823.0001","0.03010127",1],["3822.8014","0.00261588",1],["3822.7064","0.001",1],["3822.2","1",1],["3822.1121","0.35994101",1],["3821.2222","0.00261696",1],["3821","0.001",1],["3820.1268","0.001",1],["3820","1.12992803",4],["3819","0.01331195",2],["3817.5472","0.001",1],["3816","1.13807184",2],["3815.8343","0.32463428",1],["3815.7834","0.00525295",1],["3815","28.99386799",4],["3814.9676","0.001",1],["3813","0.91303023",4],["3812.388","0.002",2],["3811.2257","0.07",1],["3810","0.32573997",2],["3809.8084","0.001",1],["3809.7928","0.00262481",1],["3807.2288","0.001",1],["3806.8421","0.07003461",1],["3806","0.19",1],["3805.8041","0.05678805",1],["3805","1.01",2],["3804.6492","0.001",1],["3804.3551","0.1",1],["3803","0.005",1],["3802.22","2.05042631",1],["3802.0696","0.001",1],["3802","1.63290092",1],["3801.2257","0.07",1],["3801","57.4",3],["3800.9853","0.02492278",1],["3800.8421","0.06503533",1],["3800.7844","0.02812628",1],["3800.0001","0.00409473",1],["3800","17.91401074",15],["3799.49","0.001",1],["3799","0.1",1],["3796.9104","0.001",1],["3796","9.00128053",2],["3795.5441","0.0028",1],["3794.3308","0.001",1],["3791","55",1],["3790.7777","0.07",1],["3790","12.03238184",7],["3789","1",1],["3788","0.21110454",2],["3787.2959","9",1],["3786.592","0.001",1],["3786","9.01916822",2],["3785","12.87914268",5],["3784.0124","0.001",1],["3781.4328","0.002",2],["3781","56.3",2],["3780.7777","0.07",1],["3780","23.41537654",10],["3778.8532","0.002",2],["3776","9",1],["3774","0.003",1],["3772.2481","0.06901672",1],["3771","55.1",2],["3770.7777","0.07",1],["3770","7.30268416",5],["3769","0.25",1],["3768","1.3725",3],["3766.66","0.02",1],["3766","7.64837924",2],["3765.58","1.22775492",1],["3762.58","1.22873383",1],["3761","51.68262164",1],["3760.8031","0.0399",1],["3760.7777","0.07",1]],"timestamp":"2019-03-06T23:19:17.705Z","checksum":-1785549915}]}` update := `{"table":"spot/depth","action":"update","data":[{"instrument_id":"BTC-USDT","asks":[["3864.6786","0",0],["3864.9852","0",0],["3865.9994","0.48402971",1],["3866.4004","0.001",1],["3866.7995","0.3273",2],["3867.4566","0",0],["3867.7031","0.025",1],["3868.0436","0",0],["3868.346","0",0],["3868.3695","0.051",1],["3870.9243","0.642",1],["3874.9942","0.51751796",1],["3875.7057","0",0],["3939","0.001",1]],"bids":[["3864.55","0.0565449",1],["3863.8282","0",0],["3863.8153","0",0],["3863.7898","0.01320077",1],["3863.4807","0.02112123",1],["3863.3002","0.04233533",1],["3863.1717","0.03379397",1],["3863.0685","0.04438179",1],["3863.0286","0.7362564",1],["3862.9912","0.06773651",1],["3862.8626","0.05407035",1],["3862.7595","0.07101087",1],["3862.313","0.3756",2],["3862.1848","0.012",1],["3862.0734","0",0],["3861.8391","0.025",1],["3861.7888","0",0],["3856.6716","0.38893641",1],["3768","0",0],["3766.66","0",0],["3766","0",0],["3765.58","0",0],["3762.58","0",0],["3761","0",0],["3760.8031","0",0],["3760.7777","0",0]],"timestamp":"2019-03-06T23:19:18.239Z","checksum":-1587788848}]}` - var dataResponse okgroup.WebsocketDataResponse - err := json.Unmarshal([]byte(original), &dataResponse) + err := o.WsProcessOrderBook([]byte(original)) if err != nil { - t.Error(err) + t.Fatal(err) } - err = o.WsProcessOrderBook(&dataResponse) - if err != nil { - t.Error(err) - return - } - var updateResponse okgroup.WebsocketDataResponse - err = json.Unmarshal([]byte(update), &updateResponse) - if err != nil { - t.Error(err) - } - time.Sleep(2 * time.Second) - err = o.WsProcessOrderBook(&updateResponse) + time.Sleep(time.Second) + err = o.WsProcessOrderBook([]byte(update)) if err != nil { t.Error(err) } @@ -856,23 +845,12 @@ func TestOrderBookUpdateChecksumCalculatorWith8DecimalPlaces(t *testing.T) { } original := `{"table":"spot/depth","action":"partial","data":[{"instrument_id":"WAVES-BTC","asks":[["0.000714","1.15414979",1],["0.000715","3.3",2],["0.000717","426.71348",2],["0.000719","140.84507042",1],["0.00072","590.77",1],["0.000721","991.77",1],["0.000724","0.3532032",1],["0.000725","58.82698567",1],["0.000726","1033.15469748",2],["0.000729","0.35320321",1],["0.00073","352.77",1],["0.000735","0.38469748",1],["0.000736","625.77",1],["0.00075191","152.44796961",1],["0.00075192","114.3359772",1],["0.00075193","85.7519829",1],["0.00075194","64.31398718",1],["0.00075195","48.23549038",1],["0.00075196","36.17661779",1],["0.00075199","61.04804253",1],["0.0007591","70.71318474",1],["0.0007621","53.03488855",1],["0.00076211","39.77616642",1],["0.00076212","29.83212481",1],["0.0007635","22.37409361",1],["0.00076351","29.36599786",2],["0.00076352","9.43907074",1],["0.00076353","7.07930306",1],["0.00076354","14.15860612",1],["0.00076355","3.53965153",1],["0.00076369","3.53965153",1],["0.0008","34.36841101",1],["0.00082858","1.69936503",1],["0.00083232","2.8",1],["0.00084","15.69220129",1],["0.00085","4.42785042",1],["0.00088","0.1",1],["0.000891","0.1",1],["0.0009","12.41486491",2],["0.00093","5",1],["0.0012","12.31486492",1],["0.00531314","6.91803114",1],["0.00799999","0.02",1],["0.0084","0.05989",1],["0.00931314","5.18852336",1],["0.0799999","0.02",1],["0.499","6.00423396",1],["0.5","0.4995",1],["0.799999","0.02",1],["4.99","2",1],["5","3.98583144",1],["7.99999999","0.02",1],["79.99999999","0.02",1],["799.99999999","0.02986704",1]],"bids":[["0.000709","222.91679881",3],["0.000703","0.47161952",1],["0.000701","140.73015789",2],["0.0007","0.3",1],["0.000699","401",1],["0.000698","232.61801667",2],["0.000689","0.71396896",1],["0.000688","0.69910125",1],["0.000613","227.54771052",1],["0.0005","0.01",1],["0.00026789","3.69905341",1],["0.000238","2.4",1],["0.00022","0.53",1],["0.0000055","374.09871696",1],["0.00000056","222",1],["0.00000055","736.84761363",1],["0.0000002","999",1],["0.00000009","1222.22222417",1],["0.00000008","20868.64520447",1],["0.00000002","110000",1],["0.00000001","10000",1]],"timestamp":"2019-03-12T22:22:42.274Z","checksum":1319037905}]}` update := `{"table":"spot/depth","action":"update","data":[{"instrument_id":"WAVES-BTC","asks":[["0.000715","100.48199596",3],["0.000716","62.21679881",1]],"bids":[["0.000713","38.95772168",1]],"timestamp":"2019-03-12T22:22:42.938Z","checksum":-131160897}]}` - var dataResponse okgroup.WebsocketDataResponse - err := json.Unmarshal([]byte(original), &dataResponse) + err := o.WsProcessOrderBook([]byte(original)) if err != nil { - t.Error(err) + t.Fatal(err) } - err = o.WsProcessOrderBook(&dataResponse) - if err != nil { - t.Error(err) - return - } - var updateResponse okgroup.WebsocketDataResponse - err = json.Unmarshal([]byte(update), &updateResponse) - if err != nil { - t.Error(err) - } - time.Sleep(2 * time.Second) - err = o.WsProcessOrderBook(&updateResponse) + time.Sleep(time.Second) + err = o.WsProcessOrderBook([]byte(update)) if err != nil { t.Error(err) } @@ -881,15 +859,15 @@ func TestOrderBookUpdateChecksumCalculatorWith8DecimalPlaces(t *testing.T) { // TestOrderBookPartialChecksumCalculator logic test func TestOrderBookPartialChecksumCalculator(t *testing.T) { orderbookPartialJSON := `{"table":"spot/depth","action":"partial","data":[{"instrument_id":"EOS-USDT","asks":[["3.5196","0.1077",1],["3.5198","21.71",1],["3.5199","51.1805",1],["3.5208","75.09",1],["3.521","196.3333",1],["3.5213","0.1",1],["3.5218","39.276",2],["3.5219","395.6334",1],["3.522","27.956",1],["3.5222","404.9595",1],["3.5225","300",1],["3.5227","143.5442",2],["3.523","42.4746",1],["3.5231","852.64",2],["3.5235","34.9602",1],["3.5237","442.0918",2],["3.5238","352.8404",2],["3.5239","341.6759",2],["3.524","84.9493",1],["3.5241","148.4882",1],["3.5242","261.64",1],["3.5243","142.045",1],["3.5246","10",1],["3.5247","284.0788",1],["3.5248","720",1],["3.5249","89.2518",2],["3.5251","1201.8965",2],["3.5254","426.2938",1],["3.5255","213.0863",1],["3.5257","568.1576",1],["3.5258","0.3",1],["3.5259","34.4602",1],["3.526","0.1",1],["3.5263","850.771",1],["3.5265","5.9",1],["3.5268","10.5064",2],["3.5272","1136.8965",1],["3.5274","255.1481",1],["3.5276","29.5374",1],["3.5278","50",1],["3.5282","284.1797",1],["3.5283","1136.8965",1],["3.5284","0.4275",1],["3.5285","100",1],["3.5292","90.9",1],["3.5298","0.2",1],["3.5303","568.1576",1],["3.5305","279.9999",1],["3.532","0.409",1],["3.5321","568.1576",1],["3.5326","6016.8756",1],["3.5328","4.9849",1],["3.533","92.88",2],["3.5343","1200.2383",2],["3.5344","100",1],["3.535","359.7047",1],["3.5354","100",1],["3.5355","100",1],["3.5356","10",1],["3.5358","200",2],["3.5362","435.139",1],["3.5365","2152",1],["3.5366","284.1756",1],["3.5367","568.4644",1],["3.5369","33.9878",1],["3.537","337.1191",2],["3.5373","0.4045",1],["3.5383","1136.7188",1],["3.5386","12.1614",1],["3.5387","90.89",1],["3.54","4.54",1],["3.5423","90.8",1],["3.5436","0.1",1],["3.5454","853.4156",1],["3.5468","142.0656",1],["3.5491","0.0008",1],["3.55","14478.8206",6],["3.5537","21521",1],["3.5555","11.53",1],["3.5573","50.6001",1],["3.5599","4591.4221",1],["3.56","1227.0002",4],["3.5603","2670",1],["3.5608","58.6638",1],["3.5613","0.1",1],["3.5621","45.9473",1],["3.57","2141.7274",3],["3.5712","2956.9816",1],["3.5717","27.9978",1],["3.5718","0.9285",1],["3.5739","299.73",1],["3.5761","864",1],["3.579","22.5225",1],["3.5791","38.26",2],["3.58","7618.4634",5],["3.5801","457.2184",1],["3.582","24.5",1],["3.5822","1572.6425",1],["3.5845","14.1438",1],["3.585","527.169",1],["3.5865","20",1],["3.5867","4490",1],["3.5876","39.0493",1],["3.5879","392.9083",1],["3.5888","436.42",2],["3.5896","50",1],["3.59","2608.9128",8],["3.5913","19.5246",1],["3.5938","7082",1],["3.597","0.1",1],["3.5979","399",1],["3.5995","315.1509",1],["3.5999","2566.2648",1],["3.6","18511.2292",35],["3.603","22.3379",2],["3.605","499.5",1],["3.6055","100",1],["3.6058","499.5",1],["3.608","1021.1485",1],["3.61","11755.4596",13],["3.611","42.8571",1],["3.6131","6690",1],["3.6157","19.5247",1],["3.618","2500",1],["3.6197","525.7146",1],["3.6198","0.4455",1],["3.62","6440.6295",8],["3.6219","0.4175",1],["3.6237","168",1],["3.6265","0.1001",1],["3.628","64.9345",1],["3.63","4435.4985",6],["3.6308","1.7815",1],["3.6331","0.1",1],["3.6338","355.527",2],["3.6358","50",1],["3.6363","2074.7096",1],["3.6376","4000",1],["3.6396","11090",1],["3.6399","0.4055",1],["3.64","4161.9805",4],["3.6437","117.6524",1],["3.648","190",1],["3.6488","200",1],["3.65","11740.5045",25],["3.6512","0.1",1],["3.6521","728",1],["3.6555","100",1],["3.6598","36.6914",1],["3.66","4331.2148",6],["3.6638","200",1],["3.6673","100",1],["3.6679","38",1],["3.6688","2",1],["3.6695","0.1",1],["3.67","7984.698",6],["3.672","300",1],["3.6777","257.8247",1],["3.6789","393.4217",2],["3.68","9202.3222",11],["3.6818","500",1],["3.6823","299.7",1],["3.6839","422.3748",1],["3.685","100",1],["3.6878","0.1",1],["3.6888","72.0958",2],["3.6889","2876",1],["3.689","28",1],["3.6891","28",1],["3.6892","28",1],["3.6895","28",1],["3.6898","28",1],["3.69","643.96",7],["3.6908","118",2],["3.691","28",1],["3.6916","28",1],["3.6918","28",1],["3.6926","28",1],["3.6928","28",1],["3.6932","28",1],["3.6933","200",1],["3.6935","28",1],["3.6936","28",1],["3.6938","28",1],["3.694","28",1],["3.698","1498.5",1],["3.6988","2014.2004",2],["3.7","21904.2689",22],["3.7029","71.95",1],["3.704","3690.1362",1],["3.7055","100",1],["3.7063","0.1",1],["3.71","4421.3468",4],["3.719","17.3491",1],["3.72","1304.5995",3],["3.7211","10",1],["3.7248","0.1",1],["3.725","1900",1],["3.73","31.1785",2],["3.7375","38",1]],"bids":[["3.5182","151.5343",6],["3.5181","0.3691",1],["3.518","271.3967",2],["3.5179","257.8352",1],["3.5178","12.3811",1],["3.5173","34.1921",2],["3.5171","1013.8256",2],["3.517","272.1119",2],["3.5168","395.3376",1],["3.5166","317.1756",2],["3.5165","348.302",3],["3.5164","142.0414",1],["3.5163","96.8933",2],["3.516","600.1034",3],["3.5159","27.481",1],["3.5158","27.33",1],["3.5157","583.1898",2],["3.5156","24.6819",2],["3.5154","25",1],["3.5153","0.429",1],["3.5152","453.9204",3],["3.5151","2131.592",4],["3.515","335",3],["3.5149","37.1586",1],["3.5147","41.6759",1],["3.5146","54.569",1],["3.5145","70.3515",1],["3.5143","68.206",3],["3.5142","359.4538",2],["3.5139","45.4123",2],["3.5137","71.673",2],["3.5136","25",1],["3.5135","300",1],["3.5134","442.57",2],["3.5132","83.3518",1],["3.513","1245.2529",3],["3.5127","20",1],["3.512","284.1353",1],["3.5119","1136.8319",1],["3.5113","56.9351",1],["3.5111","588.1898",2],["3.5109","255.0946",1],["3.5105","48.65",1],["3.5103","50.2",1],["3.5098","720",1],["3.5096","148.95",1],["3.5094","570.5758",2],["3.509","2.386",1],["3.5089","0.4065",1],["3.5087","282.3859",2],["3.5086","145.036",2],["3.5084","2.386",1],["3.5082","90.98",1],["3.5081","2.386",1],["3.5079","2.386",1],["3.5078","857.6229",2],["3.5075","2.386",1],["3.5074","284.1877",1],["3.5073","100",1],["3.5071","100",1],["3.507","768.4159",3],["3.5069","313.0863",2],["3.5068","426.2938",1],["3.5066","568.3594",1],["3.5063","1136.6865",1],["3.5059","0.3",1],["3.5054","9.9999",1],["3.5053","0.2",1],["3.5051","392.428",1],["3.505","13.79",1],["3.5048","99.5497",2],["3.5047","78.5331",2],["3.5046","2153",1],["3.5041","5983.999",1],["3.5037","668.5682",1],["3.5036","160.5948",1],["3.5024","534.8075",1],["3.5014","28.5604",1],["3.5011","91",1],["3.5","1058.8771",2],["3.4997","50.2",1],["3.4985","3430.0414",1],["3.4949","232.0591",1],["3.4942","21521",1],["3.493","2",1],["3.4928","2",1],["3.4925","0.44",1],["3.4917","142.0656",1],["3.49","2051.8826",4],["3.488","280.7459",1],["3.4852","643.4038",1],["3.4851","86.0807",1],["3.485","213.2436",1],["3.484","0.1",1],["3.4811","144.3399",1],["3.4808","89",1],["3.4803","12.1999",1],["3.4801","2390",1],["3.48","930.8453",9],["3.4791","310",1],["3.4768","206",1],["3.4767","0.9415",1],["3.4754","1.4387",1],["3.4728","20",1],["3.4701","1219.2873",1],["3.47","1904.3139",7],["3.468","0.4035",1],["3.4667","0.1",1],["3.4666","3020.0101",1],["3.465","10",1],["3.464","0.4485",1],["3.462","2119.6556",1],["3.46","1305.6113",8],["3.4589","8.0228",1],["3.457","100",1],["3.456","70.3859",2],["3.4538","20",1],["3.4536","4323.9486",2],["3.4531","827.0427",1],["3.4528","0.439",1],["3.4522","8.0381",1],["3.4513","441.1873",1],["3.4512","50.707",1],["3.451","87.0902",1],["3.4509","200",1],["3.4506","100",1],["3.4505","86.4045",2],["3.45","12409.4595",28],["3.4494","0.5365",2],["3.449","10761",1],["3.4482","8.0476",1],["3.4469","0.449",1],["3.445","2000",1],["3.4427","14",1],["3.4421","100",1],["3.4416","8.0631",1],["3.4404","1",1],["3.44","4580.733",11],["3.4388","1868.2085",1],["3.438","937.7246",2],["3.4367","1500",1],["3.4366","62",1],["3.436","29.8743",1],["3.4356","25.4801",1],["3.4349","4.3086",1],["3.4343","43.2402",1],["3.433","2.0688",1],["3.4322","2.7335",2],["3.432","93.3233",1],["3.4302","328.8301",2],["3.43","4440.8158",11],["3.4288","754.574",2],["3.4283","125.7043",2],["3.428","744.3154",2],["3.4273","5460",1],["3.4258","50",1],["3.4255","109.005",1],["3.4248","100",1],["3.4241","129.2048",2],["3.4233","5.3598",1],["3.4228","4498.866",1],["3.4222","3.5435",1],["3.4217","404.3252",2],["3.4211","1000",1],["3.4208","31",1],["3.42","1834.024",9],["3.4175","300",1],["3.4162","400",1],["3.4152","0.1",1],["3.4151","4.3336",1],["3.415","1.5974",1],["3.414","1146",1],["3.4134","306.4246",1],["3.4129","7.5556",1],["3.4111","198.5188",1],["3.4109","500",1],["3.4106","4305",1],["3.41","2150.7635",13],["3.4085","4.342",1],["3.4054","5.6985",1],["3.4019","5.438",1],["3.4015","1010.846",1],["3.4009","8610",1],["3.4005","1.9122",1],["3.4004","1",1],["3.4","27081.1806",67],["3.3955","3.2682",1],["3.3953","5.4486",1],["3.3937","1591.3805",1],["3.39","3221.4155",8],["3.3899","3.2736",1],["3.3888","1500",2],["3.3887","5.4592",1],["3.385","117.0969",2],["3.3821","5.4699",1],["3.382","100.0529",1],["3.3818","172.0164",1],["3.3815","165.6288",1],["3.381","887.3115",1],["3.3808","100",1]],"timestamp":"2019-03-04T00:15:04.155Z","checksum":-2036653089}]}` - var dataResponse okgroup.WebsocketDataResponse + var dataResponse okgroup.WebsocketOrderBook err := json.Unmarshal([]byte(orderbookPartialJSON), &dataResponse) if err != nil { t.Error(err) } - calculatedChecksum := o.CalculatePartialOrderbookChecksum(&dataResponse.Data[0]) - if calculatedChecksum != dataResponse.Data[0].Checksum { - t.Errorf("Expected %v, Receieved %v", dataResponse.Data[0].Checksum, calculatedChecksum) + calculatedChecksum := o.CalculatePartialOrderbookChecksum(&dataResponse) + if calculatedChecksum != dataResponse.Checksum { + t.Errorf("Expected %v, Receieved %v", dataResponse.Checksum, calculatedChecksum) } } @@ -995,11 +973,11 @@ func TestSubmitOrder(t *testing.T) { Base: currency.BTC, Quote: currency.USD, }, - OrderSide: order.Buy, - OrderType: order.Limit, - Price: -1, - Amount: 1, - ClientID: "meowOrder", + Side: order.Buy, + Type: order.Limit, + Price: -1, + Amount: 1, + ClientID: "meowOrder", } response, err := o.SubmitOrder(orderSubmission) if areTestAPIKeysSet() && (err != nil || !response.IsOrderPlaced) { @@ -1014,10 +992,10 @@ func TestCancelExchangeOrder(t *testing.T) { TestSetRealOrderDefaults(t) currencyPair := currency.NewPair(currency.LTC, currency.BTC) var orderCancellation = order.Cancel{ - OrderID: "1", + ID: "1", WalletAddress: core.BitcoinDonationAddress, AccountID: "1", - CurrencyPair: currencyPair, + Pair: currencyPair, } err := o.CancelOrder(&orderCancellation) @@ -1029,10 +1007,10 @@ func TestCancelAllExchangeOrders(t *testing.T) { TestSetRealOrderDefaults(t) currencyPair := currency.NewPair(currency.LTC, currency.BTC) var orderCancellation = order.Cancel{ - OrderID: "1", + ID: "1", WalletAddress: core.BitcoinDonationAddress, AccountID: "1", - CurrencyPair: currencyPair, + Pair: currencyPair, } resp, err := o.CancelAllOrders(&orderCancellation) diff --git a/exchanges/okcoin/okcoin_wrapper.go b/exchanges/okcoin/okcoin_wrapper.go index d2dd4bf9..81d2e728 100644 --- a/exchanges/okcoin/okcoin_wrapper.go +++ b/exchanges/okcoin/okcoin_wrapper.go @@ -103,6 +103,9 @@ func (o *OKCoin) SetDefaults() { Unsubscribe: true, AuthenticatedEndpoints: true, MessageCorrelation: true, + GetOrders: true, + GetOrder: true, + AccountBalance: true, }, WithdrawPermissions: exchange.AutoWithdrawCrypto | exchange.NoFiatWithdrawals, diff --git a/exchanges/okex/okex_test.go b/exchanges/okex/okex_test.go index 26656610..57fa4eb6 100644 --- a/exchanges/okex/okex_test.go +++ b/exchanges/okex/okex_test.go @@ -1450,7 +1450,7 @@ func TestSendWsMessages(t *testing.T) { } wg := sync.WaitGroup{} wg.Add(1) - go o.WsHandleData(&wg) + go o.WsReadData(&wg) wg.Wait() subscription := wshandler.WebsocketChannelSubscription{ @@ -1508,23 +1508,12 @@ func TestGetWsChannelWithoutOrderType(t *testing.T) { func TestOrderBookUpdateChecksumCalculator(t *testing.T) { original := `{"table":"spot/depth","action":"partial","data":[{"instrument_id":"BTC-USDT","asks":[["3864.6786","0.145",1],["3864.7682","0.005",1],["3864.9851","0.57",1],["3864.9852","0.30137754",1],["3864.9986","2.81818419",1],["3864.9995","0.002",1],["3865","0.0597",1],["3865.0309","0.4",1],["3865.1995","0.004",1],["3865.3995","0.004",1],["3865.5995","0.004",1],["3865.7995","0.004",1],["3865.9995","0.004",1],["3866.0961","0.25865886",1],["3866.1995","0.004",1],["3866.3995","0.004",1],["3866.4004","0.3243",2],["3866.5995","0.004",1],["3866.7633","0.44247086",1],["3866.7995","0.004",1],["3866.9197","0.511",1],["3867.256","0.51716256",1],["3867.3951","0.02588112",1],["3867.4014","0.025",1],["3867.4566","0.02499999",1],["3867.4675","4.01155057",5],["3867.5515","1.1",1],["3867.6113","0.009",1],["3867.7349","0.026",1],["3867.7781","0.03738652",1],["3867.9163","0.0521",1],["3868.0381","0.34354941",1],["3868.0436","0.051",1],["3868.0657","0.90552172",3],["3868.1819","0.03863346",1],["3868.2013","0.194",1],["3868.346","0.051",1],["3868.3863","0.01155",1],["3868.7716","0.009",1],["3868.947","0.025",1],["3868.98","0.001",1],["3869.0764","1.03487931",1],["3869.2773","0.07724578",1],["3869.4039","0.025",1],["3869.4068","1.03",1],["3869.7068","2.06976398",1],["3870","0.5",1],["3870.0465","0.01",1],["3870.7042","0.02099651",1],["3870.9451","2.07047375",1],["3871.5254","1.2",1],["3871.5596","0.001",1],["3871.6605","0.01035032",1],["3871.7179","2.07047375",1],["3871.8816","0.51751625",1],["3872.1","0.75",1],["3872.2464","0.0646",1],["3872.3747","0.283",1],["3872.4039","0.2",1],["3872.7655","0.23179307",1],["3872.8005","2.06976398",1],["3873.1509","2",1],["3873.3215","0.26",1],["3874.1392","0.001",1],["3874.1487","3.88224364",4],["3874.1685","1.8",1],["3874.5571","0.08974762",1],["3874.734","2.06976398",1],["3874.99","0.3",1],["3875","1.001",2],["3875.0041","1.03505051",1],["3875.45","0.3",1],["3875.4766","0.15",1],["3875.7057","0.51751625",1],["3876","0.001",1],["3876.68","0.3",1],["3876.7188","0.001",1],["3877","0.75",1],["3877.31","0.035",1],["3877.38","0.3",1],["3877.7","0.3",1],["3877.88","0.3",1],["3878.0364","0.34770122",1],["3878.4525","0.48579748",1],["3878.4955","0.02812511",1],["3878.8855","0.00258579",1],["3878.9605","0.895",1],["3879","0.001",1],["3879.2984","0.002",2],["3879.432","0.001",1],["3879.6313","6",1],["3879.9999","0.002",2],["3880","1.25132834",5],["3880.2526","0.04075162",1],["3880.7145","0.0647",1],["3881.2469","1.883",1],["3881.878","0.002",2],["3884.4576","0.002",2],["3885","0.002",2],["3885.2233","0.28304103",1],["3885.7416","18",1],["3886","0.001",1],["3886.1554","5.4",1],["3887","0.001",1],["3887.0372","0.002",2],["3887.2559","0.05214011",1],["3887.9238","0.0019",1],["3888","0.15810538",4],["3889","0.001",1],["3889.5175","0.50510653",1],["3889.6168","0.002",2],["3889.9999","0.001",1],["3890","2.34968109",4],["3890.5222","0.00257806",1],["3891.2659","5",1],["3891.9999","0.00893897",1],["3892.1964","0.002",2],["3892.4358","0.0176",1],["3893.1388","1.4279",1],["3894","0.0026321",1],["3894.776","0.001",1],["3895","1.501",2],["3895.379","0.25881288",1],["3897","0.05",1],["3897.3556","0.001",1],["3897.8432","0.73708079",1],["3898","3.31353018",7],["3898.4462","4.757",1],["3898.6","0.47159638",1],["3898.8769","0.0129",1],["3899","6",2],["3899.6516","0.025",1],["3899.9352","0.001",1],["3899.9999","0.013",2],["3900","22.37447743",24],["3900.9999","0.07763916",1],["3901","0.10192487",1],["3902.1937","0.00257034",1],["3902.3991","1.5532141",1],["3902.5148","0.001",1],["3904","1.49331984",1],["3904.9999","0.95905447",1],["3905","0.501",2],["3905.0944","0.001",1],["3905.61","0.099",1],["3905.6801","0.54343686",1],["3906.2901","0.0258",1],["3907.674","0.001",1],["3907.85","1.35778084",1],["3908","0.03846153",1],["3908.23","1.95189531",1],["3908.906","0.03148978",1],["3909","0.001",1],["3909.9999","0.01398721",2],["3910","0.016",2],["3910.2536","0.001",1],["3912.5406","0.88270517",1],["3912.8332","0.001",1],["3913","1.2640608",1],["3913.87","1.69114184",1],["3913.9003","0.00256266",1],["3914","1.21766411",1],["3915","0.001",1],["3915.4128","0.001",1],["3915.7425","6.848",1],["3916","0.0050949",1],["3917.36","1.28658296",1],["3917.9924","0.001",1],["3919","0.001",1],["3919.9999","0.001",1],["3920","1.21171832",3],["3920.0002","0.20217038",1],["3920.572","0.001",1],["3921","0.128",1],["3923.0756","0.00148064",1],["3923.1516","0.001",1],["3923.86","1.38831714",1],["3925","0.01867801",2],["3925.642","0.00255499",1],["3925.7312","0.001",1],["3926","0.04290757",1],["3927","0.023",1],["3927.3175","0.01212865",1],["3927.65","1.51375612",1],["3928","0.5",1],["3928.3108","0.001",1],["3929","0.001",1],["3929.9999","0.01519338",2],["3930","0.0174985",3],["3930.21","1.49335799",1],["3930.8904","0.001",1],["3932.2999","0.01953",1],["3932.8962","7.96",1],["3933.0387","11.808",1],["3933.47","0.001",1],["3934","1.40839932",1],["3935","0.001",1],["3936.8","0.62879518",1],["3937.23","1.56977841",1],["3937.4189","0.00254735",1]],"bids":[["3864.5217","0.00540709",1],["3864.5216","0.14068758",2],["3864.2275","0.01033576",1],["3864.0989","0.00825047",1],["3864.0273","0.38",1],["3864.0272","0.4",1],["3863.9957","0.01083539",1],["3863.9184","0.01653723",1],["3863.8282","0.25588165",1],["3863.8153","0.154",1],["3863.7791","1.14122492",1],["3863.6866","0.01733662",1],["3863.6093","0.02645958",1],["3863.3775","0.02773862",1],["3863.0297","0.513",1],["3863.0286","1.1028564",2],["3862.8489","0.01",1],["3862.5972","0.01890179",1],["3862.3431","0.01152944",1],["3862.313","0.009",1],["3862.2445","0.90551002",3],["3862.0734","0.014",1],["3862.0539","0.64976067",1],["3861.8586","0.025",1],["3861.7888","0.025",1],["3861.7673","0.008",1],["3861.5785","0.01",1],["3861.3895","0.005",1],["3861.3338","0.25875855",1],["3861.161","0.01",1],["3861.1111","0.03863352",1],["3861.0732","0.51703882",1],["3860.9116","0.17754895",1],["3860.75","0.19",1],["3860.6554","0.015",1],["3860.6172","0.005",1],["3860.6088","0.008",1],["3860.4724","0.12940042",1],["3860.4424","0.25880084",1],["3860.42","0.01",1],["3860.3725","0.51760102",1],["3859.8449","0.005",1],["3859.8285","0.03738652",1],["3859.7638","0.07726703",1],["3859.4502","0.008",1],["3859.3772","0.05173471",1],["3859.3409","0.194",1],["3859","5",1],["3858.827","0.0521",1],["3858.8208","0.001",1],["3858.679","0.26",1],["3858.4814","0.07477305",1],["3858.1669","1.03503422",1],["3857.6005","0.006",1],["3857.4005","0.004",1],["3857.2005","0.004",1],["3857.1871","1.218",1],["3857.0005","0.004",1],["3856.8135","0.0646",1],["3856.8005","0.004",1],["3856.2412","0.001",1],["3856.2349","1.03503422",1],["3856.0197","0.01037339",1],["3855.8781","0.23178117",1],["3855.8005","0.004",1],["3855.7165","0.00259355",1],["3855.4858","0.25875855",1],["3854.4584","0.01",1],["3853.6616","0.001",1],["3853.1373","0.92",1],["3852.5072","0.48599702",1],["3851.3926","0.13008333",1],["3851.082","0.001",1],["3850.9317","2",1],["3850.6359","0.34770165",1],["3850.2058","0.51751624",1],["3850.0823","0.15",1],["3850.0042","0.5175171",1],["3850","0.001",1],["3849.6325","1.8",1],["3849.41","0.3",1],["3848.9686","1.85",1],["3848.7426","0.18511466",1],["3848.52","0.3",1],["3848.5024","0.001",1],["3848.42","0.3",1],["3848.1618","2.204",1],["3847.77","0.3",1],["3847.48","0.3",1],["3847.3581","2.05",1],["3846.8259","0.0646",1],["3846.59","0.3",1],["3846.49","0.3",1],["3845.9228","0.001",1],["3844.184","0.00260133",1],["3844.0092","6.3",1],["3843.3432","0.001",1],["3841","0.06300963",1],["3840.7636","0.001",1],["3840","0.201",3],["3839.7681","18",1],["3839.5328","0.05214011",1],["3838.184","0.001",1],["3837.2344","0.27589557",1],["3836.6479","5.2",1],["3836","2.37196773",3],["3835.6044","0.001",1],["3833.6053","0.25873556",1],["3833.0248","0.001",1],["3833","0.8726502",1],["3832.6859","0.00260913",1],["3832","0.007",1],["3831.637","6",1],["3831.0602","0.001",1],["3830.4452","0.001",1],["3830","0.20375718",4],["3829.7125","0.07833486",1],["3829.6283","0.3519681",1],["3829","0.0039261",1],["3827.8656","0.001",1],["3826.0001","0.53251232",1],["3826","0.0509",1],["3825.7834","0.00698562",1],["3825.286","0.001",1],["3823.0001","0.03010127",1],["3822.8014","0.00261588",1],["3822.7064","0.001",1],["3822.2","1",1],["3822.1121","0.35994101",1],["3821.2222","0.00261696",1],["3821","0.001",1],["3820.1268","0.001",1],["3820","1.12992803",4],["3819","0.01331195",2],["3817.5472","0.001",1],["3816","1.13807184",2],["3815.8343","0.32463428",1],["3815.7834","0.00525295",1],["3815","28.99386799",4],["3814.9676","0.001",1],["3813","0.91303023",4],["3812.388","0.002",2],["3811.2257","0.07",1],["3810","0.32573997",2],["3809.8084","0.001",1],["3809.7928","0.00262481",1],["3807.2288","0.001",1],["3806.8421","0.07003461",1],["3806","0.19",1],["3805.8041","0.05678805",1],["3805","1.01",2],["3804.6492","0.001",1],["3804.3551","0.1",1],["3803","0.005",1],["3802.22","2.05042631",1],["3802.0696","0.001",1],["3802","1.63290092",1],["3801.2257","0.07",1],["3801","57.4",3],["3800.9853","0.02492278",1],["3800.8421","0.06503533",1],["3800.7844","0.02812628",1],["3800.0001","0.00409473",1],["3800","17.91401074",15],["3799.49","0.001",1],["3799","0.1",1],["3796.9104","0.001",1],["3796","9.00128053",2],["3795.5441","0.0028",1],["3794.3308","0.001",1],["3791","55",1],["3790.7777","0.07",1],["3790","12.03238184",7],["3789","1",1],["3788","0.21110454",2],["3787.2959","9",1],["3786.592","0.001",1],["3786","9.01916822",2],["3785","12.87914268",5],["3784.0124","0.001",1],["3781.4328","0.002",2],["3781","56.3",2],["3780.7777","0.07",1],["3780","23.41537654",10],["3778.8532","0.002",2],["3776","9",1],["3774","0.003",1],["3772.2481","0.06901672",1],["3771","55.1",2],["3770.7777","0.07",1],["3770","7.30268416",5],["3769","0.25",1],["3768","1.3725",3],["3766.66","0.02",1],["3766","7.64837924",2],["3765.58","1.22775492",1],["3762.58","1.22873383",1],["3761","51.68262164",1],["3760.8031","0.0399",1],["3760.7777","0.07",1]],"timestamp":"2019-03-06T23:19:17.705Z","checksum":-1785549915}]}` update := `{"table":"spot/depth","action":"update","data":[{"instrument_id":"BTC-USDT","asks":[["3864.6786","0",0],["3864.9852","0",0],["3865.9994","0.48402971",1],["3866.4004","0.001",1],["3866.7995","0.3273",2],["3867.4566","0",0],["3867.7031","0.025",1],["3868.0436","0",0],["3868.346","0",0],["3868.3695","0.051",1],["3870.9243","0.642",1],["3874.9942","0.51751796",1],["3875.7057","0",0],["3939","0.001",1]],"bids":[["3864.55","0.0565449",1],["3863.8282","0",0],["3863.8153","0",0],["3863.7898","0.01320077",1],["3863.4807","0.02112123",1],["3863.3002","0.04233533",1],["3863.1717","0.03379397",1],["3863.0685","0.04438179",1],["3863.0286","0.7362564",1],["3862.9912","0.06773651",1],["3862.8626","0.05407035",1],["3862.7595","0.07101087",1],["3862.313","0.3756",2],["3862.1848","0.012",1],["3862.0734","0",0],["3861.8391","0.025",1],["3861.7888","0",0],["3856.6716","0.38893641",1],["3768","0",0],["3766.66","0",0],["3766","0",0],["3765.58","0",0],["3762.58","0",0],["3761","0",0],["3760.8031","0",0],["3760.7777","0",0]],"timestamp":"2019-03-06T23:19:18.239Z","checksum":-1587788848}]}` - var dataResponse okgroup.WebsocketDataResponse - err := json.Unmarshal([]byte(original), &dataResponse) + err := o.WsHandleData([]byte(original)) if err != nil { - t.Error(err) + t.Fatal(err) } - err = o.WsProcessOrderBook(&dataResponse) - if err != nil { - t.Error(err) - return - } - var updateResponse okgroup.WebsocketDataResponse - err = json.Unmarshal([]byte(update), &updateResponse) - if err != nil { - t.Error(err) - } - time.Sleep(2 * time.Second) - err = o.WsProcessOrderBook(&updateResponse) + time.Sleep(time.Second) + err = o.WsHandleData([]byte(update)) if err != nil { t.Error(err) } @@ -1534,23 +1523,12 @@ func TestOrderBookUpdateChecksumCalculator(t *testing.T) { func TestOrderBookUpdateChecksumCalculatorWith8DecimalPlaces(t *testing.T) { original := `{"table":"spot/depth","action":"partial","data":[{"instrument_id":"WAVES-BTC","asks":[["0.000714","1.15414979",1],["0.000715","3.3",2],["0.000717","426.71348",2],["0.000719","140.84507042",1],["0.00072","590.77",1],["0.000721","991.77",1],["0.000724","0.3532032",1],["0.000725","58.82698567",1],["0.000726","1033.15469748",2],["0.000729","0.35320321",1],["0.00073","352.77",1],["0.000735","0.38469748",1],["0.000736","625.77",1],["0.00075191","152.44796961",1],["0.00075192","114.3359772",1],["0.00075193","85.7519829",1],["0.00075194","64.31398718",1],["0.00075195","48.23549038",1],["0.00075196","36.17661779",1],["0.00075199","61.04804253",1],["0.0007591","70.71318474",1],["0.0007621","53.03488855",1],["0.00076211","39.77616642",1],["0.00076212","29.83212481",1],["0.0007635","22.37409361",1],["0.00076351","29.36599786",2],["0.00076352","9.43907074",1],["0.00076353","7.07930306",1],["0.00076354","14.15860612",1],["0.00076355","3.53965153",1],["0.00076369","3.53965153",1],["0.0008","34.36841101",1],["0.00082858","1.69936503",1],["0.00083232","2.8",1],["0.00084","15.69220129",1],["0.00085","4.42785042",1],["0.00088","0.1",1],["0.000891","0.1",1],["0.0009","12.41486491",2],["0.00093","5",1],["0.0012","12.31486492",1],["0.00531314","6.91803114",1],["0.00799999","0.02",1],["0.0084","0.05989",1],["0.00931314","5.18852336",1],["0.0799999","0.02",1],["0.499","6.00423396",1],["0.5","0.4995",1],["0.799999","0.02",1],["4.99","2",1],["5","3.98583144",1],["7.99999999","0.02",1],["79.99999999","0.02",1],["799.99999999","0.02986704",1]],"bids":[["0.000709","222.91679881",3],["0.000703","0.47161952",1],["0.000701","140.73015789",2],["0.0007","0.3",1],["0.000699","401",1],["0.000698","232.61801667",2],["0.000689","0.71396896",1],["0.000688","0.69910125",1],["0.000613","227.54771052",1],["0.0005","0.01",1],["0.00026789","3.69905341",1],["0.000238","2.4",1],["0.00022","0.53",1],["0.0000055","374.09871696",1],["0.00000056","222",1],["0.00000055","736.84761363",1],["0.0000002","999",1],["0.00000009","1222.22222417",1],["0.00000008","20868.64520447",1],["0.00000002","110000",1],["0.00000001","10000",1]],"timestamp":"2019-03-12T22:22:42.274Z","checksum":1319037905}]}` update := `{"table":"spot/depth","action":"update","data":[{"instrument_id":"WAVES-BTC","asks":[["0.000715","100.48199596",3],["0.000716","62.21679881",1]],"bids":[["0.000713","38.95772168",1]],"timestamp":"2019-03-12T22:22:42.938Z","checksum":-131160897}]}` - var dataResponse okgroup.WebsocketDataResponse - err := json.Unmarshal([]byte(original), &dataResponse) + err := o.WsHandleData([]byte(original)) if err != nil { - t.Error(err) + t.Fatal(err) } - err = o.WsProcessOrderBook(&dataResponse) - if err != nil { - t.Error(err) - return - } - var updateResponse okgroup.WebsocketDataResponse - err = json.Unmarshal([]byte(update), &updateResponse) - if err != nil { - t.Error(err) - } - time.Sleep(2 * time.Second) - err = o.WsProcessOrderBook(&updateResponse) + time.Sleep(time.Second) + err = o.WsHandleData([]byte(update)) if err != nil { t.Error(err) } @@ -1559,15 +1537,15 @@ func TestOrderBookUpdateChecksumCalculatorWith8DecimalPlaces(t *testing.T) { // TestOrderBookPartialChecksumCalculator logic test func TestOrderBookPartialChecksumCalculator(t *testing.T) { orderbookPartialJSON := `{"table":"spot/depth","action":"partial","data":[{"instrument_id":"EOS-USDT","asks":[["3.5196","0.1077",1],["3.5198","21.71",1],["3.5199","51.1805",1],["3.5208","75.09",1],["3.521","196.3333",1],["3.5213","0.1",1],["3.5218","39.276",2],["3.5219","395.6334",1],["3.522","27.956",1],["3.5222","404.9595",1],["3.5225","300",1],["3.5227","143.5442",2],["3.523","42.4746",1],["3.5231","852.64",2],["3.5235","34.9602",1],["3.5237","442.0918",2],["3.5238","352.8404",2],["3.5239","341.6759",2],["3.524","84.9493",1],["3.5241","148.4882",1],["3.5242","261.64",1],["3.5243","142.045",1],["3.5246","10",1],["3.5247","284.0788",1],["3.5248","720",1],["3.5249","89.2518",2],["3.5251","1201.8965",2],["3.5254","426.2938",1],["3.5255","213.0863",1],["3.5257","568.1576",1],["3.5258","0.3",1],["3.5259","34.4602",1],["3.526","0.1",1],["3.5263","850.771",1],["3.5265","5.9",1],["3.5268","10.5064",2],["3.5272","1136.8965",1],["3.5274","255.1481",1],["3.5276","29.5374",1],["3.5278","50",1],["3.5282","284.1797",1],["3.5283","1136.8965",1],["3.5284","0.4275",1],["3.5285","100",1],["3.5292","90.9",1],["3.5298","0.2",1],["3.5303","568.1576",1],["3.5305","279.9999",1],["3.532","0.409",1],["3.5321","568.1576",1],["3.5326","6016.8756",1],["3.5328","4.9849",1],["3.533","92.88",2],["3.5343","1200.2383",2],["3.5344","100",1],["3.535","359.7047",1],["3.5354","100",1],["3.5355","100",1],["3.5356","10",1],["3.5358","200",2],["3.5362","435.139",1],["3.5365","2152",1],["3.5366","284.1756",1],["3.5367","568.4644",1],["3.5369","33.9878",1],["3.537","337.1191",2],["3.5373","0.4045",1],["3.5383","1136.7188",1],["3.5386","12.1614",1],["3.5387","90.89",1],["3.54","4.54",1],["3.5423","90.8",1],["3.5436","0.1",1],["3.5454","853.4156",1],["3.5468","142.0656",1],["3.5491","0.0008",1],["3.55","14478.8206",6],["3.5537","21521",1],["3.5555","11.53",1],["3.5573","50.6001",1],["3.5599","4591.4221",1],["3.56","1227.0002",4],["3.5603","2670",1],["3.5608","58.6638",1],["3.5613","0.1",1],["3.5621","45.9473",1],["3.57","2141.7274",3],["3.5712","2956.9816",1],["3.5717","27.9978",1],["3.5718","0.9285",1],["3.5739","299.73",1],["3.5761","864",1],["3.579","22.5225",1],["3.5791","38.26",2],["3.58","7618.4634",5],["3.5801","457.2184",1],["3.582","24.5",1],["3.5822","1572.6425",1],["3.5845","14.1438",1],["3.585","527.169",1],["3.5865","20",1],["3.5867","4490",1],["3.5876","39.0493",1],["3.5879","392.9083",1],["3.5888","436.42",2],["3.5896","50",1],["3.59","2608.9128",8],["3.5913","19.5246",1],["3.5938","7082",1],["3.597","0.1",1],["3.5979","399",1],["3.5995","315.1509",1],["3.5999","2566.2648",1],["3.6","18511.2292",35],["3.603","22.3379",2],["3.605","499.5",1],["3.6055","100",1],["3.6058","499.5",1],["3.608","1021.1485",1],["3.61","11755.4596",13],["3.611","42.8571",1],["3.6131","6690",1],["3.6157","19.5247",1],["3.618","2500",1],["3.6197","525.7146",1],["3.6198","0.4455",1],["3.62","6440.6295",8],["3.6219","0.4175",1],["3.6237","168",1],["3.6265","0.1001",1],["3.628","64.9345",1],["3.63","4435.4985",6],["3.6308","1.7815",1],["3.6331","0.1",1],["3.6338","355.527",2],["3.6358","50",1],["3.6363","2074.7096",1],["3.6376","4000",1],["3.6396","11090",1],["3.6399","0.4055",1],["3.64","4161.9805",4],["3.6437","117.6524",1],["3.648","190",1],["3.6488","200",1],["3.65","11740.5045",25],["3.6512","0.1",1],["3.6521","728",1],["3.6555","100",1],["3.6598","36.6914",1],["3.66","4331.2148",6],["3.6638","200",1],["3.6673","100",1],["3.6679","38",1],["3.6688","2",1],["3.6695","0.1",1],["3.67","7984.698",6],["3.672","300",1],["3.6777","257.8247",1],["3.6789","393.4217",2],["3.68","9202.3222",11],["3.6818","500",1],["3.6823","299.7",1],["3.6839","422.3748",1],["3.685","100",1],["3.6878","0.1",1],["3.6888","72.0958",2],["3.6889","2876",1],["3.689","28",1],["3.6891","28",1],["3.6892","28",1],["3.6895","28",1],["3.6898","28",1],["3.69","643.96",7],["3.6908","118",2],["3.691","28",1],["3.6916","28",1],["3.6918","28",1],["3.6926","28",1],["3.6928","28",1],["3.6932","28",1],["3.6933","200",1],["3.6935","28",1],["3.6936","28",1],["3.6938","28",1],["3.694","28",1],["3.698","1498.5",1],["3.6988","2014.2004",2],["3.7","21904.2689",22],["3.7029","71.95",1],["3.704","3690.1362",1],["3.7055","100",1],["3.7063","0.1",1],["3.71","4421.3468",4],["3.719","17.3491",1],["3.72","1304.5995",3],["3.7211","10",1],["3.7248","0.1",1],["3.725","1900",1],["3.73","31.1785",2],["3.7375","38",1]],"bids":[["3.5182","151.5343",6],["3.5181","0.3691",1],["3.518","271.3967",2],["3.5179","257.8352",1],["3.5178","12.3811",1],["3.5173","34.1921",2],["3.5171","1013.8256",2],["3.517","272.1119",2],["3.5168","395.3376",1],["3.5166","317.1756",2],["3.5165","348.302",3],["3.5164","142.0414",1],["3.5163","96.8933",2],["3.516","600.1034",3],["3.5159","27.481",1],["3.5158","27.33",1],["3.5157","583.1898",2],["3.5156","24.6819",2],["3.5154","25",1],["3.5153","0.429",1],["3.5152","453.9204",3],["3.5151","2131.592",4],["3.515","335",3],["3.5149","37.1586",1],["3.5147","41.6759",1],["3.5146","54.569",1],["3.5145","70.3515",1],["3.5143","68.206",3],["3.5142","359.4538",2],["3.5139","45.4123",2],["3.5137","71.673",2],["3.5136","25",1],["3.5135","300",1],["3.5134","442.57",2],["3.5132","83.3518",1],["3.513","1245.2529",3],["3.5127","20",1],["3.512","284.1353",1],["3.5119","1136.8319",1],["3.5113","56.9351",1],["3.5111","588.1898",2],["3.5109","255.0946",1],["3.5105","48.65",1],["3.5103","50.2",1],["3.5098","720",1],["3.5096","148.95",1],["3.5094","570.5758",2],["3.509","2.386",1],["3.5089","0.4065",1],["3.5087","282.3859",2],["3.5086","145.036",2],["3.5084","2.386",1],["3.5082","90.98",1],["3.5081","2.386",1],["3.5079","2.386",1],["3.5078","857.6229",2],["3.5075","2.386",1],["3.5074","284.1877",1],["3.5073","100",1],["3.5071","100",1],["3.507","768.4159",3],["3.5069","313.0863",2],["3.5068","426.2938",1],["3.5066","568.3594",1],["3.5063","1136.6865",1],["3.5059","0.3",1],["3.5054","9.9999",1],["3.5053","0.2",1],["3.5051","392.428",1],["3.505","13.79",1],["3.5048","99.5497",2],["3.5047","78.5331",2],["3.5046","2153",1],["3.5041","5983.999",1],["3.5037","668.5682",1],["3.5036","160.5948",1],["3.5024","534.8075",1],["3.5014","28.5604",1],["3.5011","91",1],["3.5","1058.8771",2],["3.4997","50.2",1],["3.4985","3430.0414",1],["3.4949","232.0591",1],["3.4942","21521",1],["3.493","2",1],["3.4928","2",1],["3.4925","0.44",1],["3.4917","142.0656",1],["3.49","2051.8826",4],["3.488","280.7459",1],["3.4852","643.4038",1],["3.4851","86.0807",1],["3.485","213.2436",1],["3.484","0.1",1],["3.4811","144.3399",1],["3.4808","89",1],["3.4803","12.1999",1],["3.4801","2390",1],["3.48","930.8453",9],["3.4791","310",1],["3.4768","206",1],["3.4767","0.9415",1],["3.4754","1.4387",1],["3.4728","20",1],["3.4701","1219.2873",1],["3.47","1904.3139",7],["3.468","0.4035",1],["3.4667","0.1",1],["3.4666","3020.0101",1],["3.465","10",1],["3.464","0.4485",1],["3.462","2119.6556",1],["3.46","1305.6113",8],["3.4589","8.0228",1],["3.457","100",1],["3.456","70.3859",2],["3.4538","20",1],["3.4536","4323.9486",2],["3.4531","827.0427",1],["3.4528","0.439",1],["3.4522","8.0381",1],["3.4513","441.1873",1],["3.4512","50.707",1],["3.451","87.0902",1],["3.4509","200",1],["3.4506","100",1],["3.4505","86.4045",2],["3.45","12409.4595",28],["3.4494","0.5365",2],["3.449","10761",1],["3.4482","8.0476",1],["3.4469","0.449",1],["3.445","2000",1],["3.4427","14",1],["3.4421","100",1],["3.4416","8.0631",1],["3.4404","1",1],["3.44","4580.733",11],["3.4388","1868.2085",1],["3.438","937.7246",2],["3.4367","1500",1],["3.4366","62",1],["3.436","29.8743",1],["3.4356","25.4801",1],["3.4349","4.3086",1],["3.4343","43.2402",1],["3.433","2.0688",1],["3.4322","2.7335",2],["3.432","93.3233",1],["3.4302","328.8301",2],["3.43","4440.8158",11],["3.4288","754.574",2],["3.4283","125.7043",2],["3.428","744.3154",2],["3.4273","5460",1],["3.4258","50",1],["3.4255","109.005",1],["3.4248","100",1],["3.4241","129.2048",2],["3.4233","5.3598",1],["3.4228","4498.866",1],["3.4222","3.5435",1],["3.4217","404.3252",2],["3.4211","1000",1],["3.4208","31",1],["3.42","1834.024",9],["3.4175","300",1],["3.4162","400",1],["3.4152","0.1",1],["3.4151","4.3336",1],["3.415","1.5974",1],["3.414","1146",1],["3.4134","306.4246",1],["3.4129","7.5556",1],["3.4111","198.5188",1],["3.4109","500",1],["3.4106","4305",1],["3.41","2150.7635",13],["3.4085","4.342",1],["3.4054","5.6985",1],["3.4019","5.438",1],["3.4015","1010.846",1],["3.4009","8610",1],["3.4005","1.9122",1],["3.4004","1",1],["3.4","27081.1806",67],["3.3955","3.2682",1],["3.3953","5.4486",1],["3.3937","1591.3805",1],["3.39","3221.4155",8],["3.3899","3.2736",1],["3.3888","1500",2],["3.3887","5.4592",1],["3.385","117.0969",2],["3.3821","5.4699",1],["3.382","100.0529",1],["3.3818","172.0164",1],["3.3815","165.6288",1],["3.381","887.3115",1],["3.3808","100",1]],"timestamp":"2019-03-04T00:15:04.155Z","checksum":-2036653089}]}` - var dataResponse okgroup.WebsocketDataResponse + var dataResponse okgroup.WebsocketOrderBook err := json.Unmarshal([]byte(orderbookPartialJSON), &dataResponse) if err != nil { t.Error(err) } - calculatedChecksum := o.CalculatePartialOrderbookChecksum(&dataResponse.Data[0]) - if calculatedChecksum != dataResponse.Data[0].Checksum { - t.Errorf("Expected %v, Receieved %v", dataResponse.Data[0].Checksum, calculatedChecksum) + calculatedChecksum := o.CalculatePartialOrderbookChecksum(&dataResponse) + if calculatedChecksum != dataResponse.Checksum { + t.Errorf("Expected %v, Receieved %v", dataResponse.Checksum, calculatedChecksum) } } @@ -1681,11 +1659,11 @@ func TestSubmitOrder(t *testing.T) { Base: currency.BTC, Quote: currency.USDT, }, - OrderSide: order.Buy, - OrderType: order.Limit, - Price: 1, - Amount: 1, - ClientID: "meowOrder", + Side: order.Buy, + Type: order.Limit, + Price: 1, + Amount: 1, + ClientID: "meowOrder", } response, err := o.SubmitOrder(orderSubmission) if areTestAPIKeysSet() && (err != nil || !response.IsOrderPlaced) { @@ -1701,10 +1679,10 @@ func TestCancelExchangeOrder(t *testing.T) { t.Parallel() currencyPair := currency.NewPair(currency.LTC, currency.BTC) var orderCancellation = order.Cancel{ - OrderID: "1", + ID: "1", WalletAddress: core.BitcoinDonationAddress, AccountID: "1", - CurrencyPair: currencyPair, + Pair: currencyPair, } err := o.CancelOrder(&orderCancellation) @@ -1717,10 +1695,10 @@ func TestCancelAllExchangeOrders(t *testing.T) { t.Parallel() currencyPair := currency.NewPair(currency.LTC, currency.BTC) var orderCancellation = order.Cancel{ - OrderID: "1", + ID: "1", WalletAddress: core.BitcoinDonationAddress, AccountID: "1", - CurrencyPair: currencyPair, + Pair: currencyPair, } resp, err := o.CancelAllOrders(&orderCancellation) @@ -1814,3 +1792,307 @@ func TestGetOrderbook(t *testing.T) { t.Error(err) } } + +func TestWsSubscribe(t *testing.T) { + pressXToJSON := []byte(`{"event":"subscribe","channel":"spot/ticker:ETH-USDT"}`) + err := o.WsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsUnsubscribe(t *testing.T) { + pressXToJSON := []byte(`{"event":"unsubscribe","channel":"spot/candle60s:BTC-USDT"}`) + err := o.WsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsCandle(t *testing.T) { + pressXToJSON := []byte(`{ + "table":"spot/candle60s", + "data":[ + { + "candle":[ + "2019-04-16T10:49:00.000Z", + "162.03", + "162.04", + "161.96", + "161.98", + "336.452694" + ], + "instrument_id":"ETH-USDT" + } + ] +}`) + err := o.WsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsLogin(t *testing.T) { + pressXToJSON := []byte(`{"event":"login","success":"true"}`) + err := o.WsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsAccount(t *testing.T) { + pressXToJSON := []byte(`{ + "table":"spot/account", + "data":[ + { + "balance":"2.215374581132125", + "available":"1.632774581132125", + "currency":"USDT", + "id":"", + "hold":"0.5826" + } + ] +}`) + err := o.WsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsMargin(t *testing.T) { + pressXToJSON := []byte(`{ + "table": "spot/margin_account", + "data": [{ + "currency:USDT": { + "available": "0.00000000930213", + "balance": "0.00000000930213", + "borrowed": "0", + "hold": "0", + "lending_fee": "0" + }, + "liquidation_price":"4.6499", + "tiers": "1", + "maint_margin_ratio": "0.08", + "instrument_id": "ETH-USDT", + "currency:ETH": { + "available": "0.0202516022462802", + "balance": "0.0202516022462802", + "borrowed": "0.01", + "hold": "0", + "lending_fee": "0.0000001666" + } + }] +}`) + err := o.WsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsUserOrders(t *testing.T) { + pressXToJSON := []byte(`{ + "table":"spot/order", + "data":[ + { + "client_oid":"", + "filled_notional":"0", + "filled_size":"0", + "instrument_id":"ETC-USDT", + "last_fill_px":"0", + "last_fill_qty":"0", + "last_fill_time":"1970-01-01T00:00:00.000Z", + "margin_trading":"1", + "notional":"", + "order_id":"3576398568830976", + "order_type":"0", + "price":"5.826", + "side":"buy", + "size":"0.1", + "state":"0", + "status":"open", + "timestamp":"2019-09-24T06:45:11.394Z", + "type":"limit", + "created_at":"2019-09-24T06:45:11.394Z" + } + ] +}`) + err := o.WsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsAlgoOrders(t *testing.T) { + pressXToJSON := []byte(`{ + "table":"spot/order_algo", + "data":[ + { + "algo_id":"456154", + "algo_price":"15", + "cancel_code":"", + "created_at":"2020-01-08T02:42:36.791Z", + "instrument_id":"ltc_usdt", + "mode":"1", + "order_id":"0", + "order_type":"1", + "side":"buy", + "size":"3", + "status":"1", + "stop_type":"2", + "timestamp":"2020-01-08T02:42:36.796Z", + "trigger_price":"20" + } + ] +}`) + err := o.WsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsTicker(t *testing.T) { + pressXToJSON := []byte(`{ + "table":"spot/ticker", + "data":[ + { + "instrument_id":"ETH-USDT", + "last":"146.24", + "last_qty":"0.082483", + "best_bid":"146.24", + "best_bid_size":"0.006822", + "best_ask":"146.25", + "best_ask_size":"80.541709", + "open_24h":"147.17", + "high_24h":"147.48", + "low_24h":"143.88", + "base_volume_24h":"117387.58", + "quote_volume_24h":"17159427.21", + "timestamp":"2019-12-11T02:31:40.436Z" + } + ] +}`) + err := o.WsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsTrade(t *testing.T) { + pressXToJSON := []byte(`{ + "table": "spot/trade", + "data": + [{ + "instrument_id": "ETH-USDT", + "price": "22888", + "side": "buy", + "size": "7", + "timestamp": "2018-11-22T03:58:57.709Z", + "trade_id": "108223090144493569" + }] +}`) + err := o.WsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsDepth(t *testing.T) { + pressXToJSON := []byte(`{ + "table":"spot/depth5", + "data":[ + { + "asks":[ + [ + "161.96", + "7.37567", + 3 + ], + [ + "161.99", + "5.185", + 2 + ], + [ + "162", + "29.184592", + 5 + ] + ], + "bids":[ + [ + "161.94", + "4.552355", + 1 + ], + [ + "161.89", + "11.999998", + 1 + ], + [ + "161.88", + "6.585142", + 3 + ] + ], + "instrument_id":"ETH-USDT", + "timestamp":"2019-04-16T11:03:03.712Z" + } + ] +}`) + err := o.WsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsDepthByTick(t *testing.T) { + pressXToJSON := []byte(`{ + "table":"spot/depth_l2_tbt", + "action":"partial", + "data":[ + { + "instrument_id":"BTC-USDT", + "asks":[ + ["9580.3","0.20939963","0","2"], + ["9582.7","0.33242846","0","3"], + ["9583.9","0.41760039","0","1"] + ], + "bids":[ + ["9576.7","0.31658067","0","2"], + ["9574.4","0.15659893","0","2"], + ["9574.2","0.0105","0","1"] + ], + "timestamp":"2020-02-06T03:35:42.492Z", + "checksum":-2144245240 + } + ] +}`) + err := o.WsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestStringToOrderStatus(t *testing.T) { + type TestCases struct { + Case int64 + Result order.Status + } + testCases := []TestCases{ + {Case: -2, Result: order.Rejected}, + {Case: -1, Result: order.Cancelled}, + {Case: 0, Result: order.Active}, + {Case: 1, Result: order.PartiallyFilled}, + {Case: 2, Result: order.Filled}, + {Case: 3, Result: order.New}, + {Case: 4, Result: order.PendingCancel}, + {Case: 5, Result: order.UnknownStatus}, + } + for i := range testCases { + result, _ := okgroup.StringToOrderStatus(testCases[i].Case) + if result != testCases[i].Result { + t.Errorf("Exepcted: %v, received: %v", testCases[i].Result, result) + } + } +} diff --git a/exchanges/okex/okex_wrapper.go b/exchanges/okex/okex_wrapper.go index 2bdda61c..8368ee1b 100644 --- a/exchanges/okex/okex_wrapper.go +++ b/exchanges/okex/okex_wrapper.go @@ -137,6 +137,9 @@ func (o *OKEX) SetDefaults() { Unsubscribe: true, AuthenticatedEndpoints: true, MessageCorrelation: true, + GetOrders: true, + GetOrder: true, + AccountBalance: true, }, WithdrawPermissions: exchange.AutoWithdrawCrypto | exchange.NoFiatWithdrawals, diff --git a/exchanges/okgroup/okgroup_types.go b/exchanges/okgroup/okgroup_types.go index 6a64b4a1..2dea7f3b 100644 --- a/exchanges/okgroup/okgroup_types.go +++ b/exchanges/okgroup/okgroup_types.go @@ -1306,93 +1306,74 @@ type WebsocketEventResponse struct { // WebsocketDataResponse formats all response data for a websocket event type WebsocketDataResponse struct { - Table string `json:"table"` - Action string `json:"action,omitempty"` - Data []WebsocketDataWrapper `json:"data"` -} - -// WebsocketDataWrapper holds all data responses for websocket -// Can review in future if struct becomes too large -// allows for easy data processing -type WebsocketDataWrapper struct { - InstrumentID string `json:"instrument_id"` - Timestamp time.Time `json:"timestamp,omitempty"` - WebsocketTickerData - WebsocketCandleResponse - WebsocketOrderBooksData - WebsocketTradeResponse - WebsocketFundingFeeResponse - WebsocketMarkPriceResponse - WebsocketEstimatedPriceResponse - WebsocketPriceRangeResponse - WebsocketUserSwapPositionResponse - WebsocketUserSwapOrdersResponse - WebsocketUserSwapFutureAccountResponse - WebsocketUserSpotAccountResponse - WebsocketSpotMarginOrderResponse - WebsocketUserFutureFixedMarginAccountResponse - WebsocketUserFuturePositionResponse - WebsocketSpotOrderResponse + Table string `json:"table"` + Action string `json:"action,omitempty"` + Data []interface{} `json:"data"` } // WebsocketTickerData contains formatted data for ticker related websocket responses type WebsocketTickerData struct { - BaseVolume24h float64 `json:"base_volume_24h,string,omitempty"` - BestAsk float64 `json:"best_ask,string,omitempty"` - BestBid float64 `json:"best_bid,string,omitempty"` - High24h float64 `json:"high_24h,string,omitempty"` - Last float64 `json:"last,string,omitempty"` - Low24h float64 `json:"low_24h,string,omitempty"` - Open24h float64 `json:"open_24h,string,omitempty"` - QuoteVolume24h float64 `json:"quote_volume_24h,string,omitempty"` + Table string `json:"table"` + Data []struct { + BaseVolume24h float64 `json:"base_volume_24h,string"` + BestAsk float64 `json:"best_ask,string"` + BestAskSize float64 `json:"best_ask_size,string"` + BestBid float64 `json:"best_bid,string"` + BestBidSize float64 `json:"best_bid_size,string"` + High24h float64 `json:"high_24h,string"` + InstrumentID string `json:"instrument_id"` + Last float64 `json:"last,string"` + LastQty float64 `json:"last_qty,string"` + Low24h float64 `json:"low_24h,string"` + Open24h float64 `json:"open_24h,string"` + QuoteVolume24h float64 `json:"quote_volume_24h,string"` + Timestamp time.Time `json:"timestamp"` + } `json:"data"` } // WebsocketTradeResponse contains formatted data for trade related websocket responses type WebsocketTradeResponse struct { - Price float64 `json:"price,string,omitempty"` - Side string `json:"side,omitempty"` - Qty float64 `json:"qty,string,omitempty"` - TradeID string `json:"trade_id,omitempty"` + Table string `json:"table"` + Data []struct { + Price float64 `json:"price,string"` + Size float64 `json:"size,string"` + InstrumentID string `json:"instrument_id"` + Side string `json:"side"` + Timestamp time.Time `json:"timestamp"` + TradeID string `json:"trade_id"` + } `json:"data"` } // WebsocketCandleResponse contains formatted data for candle related websocket responses type WebsocketCandleResponse struct { - Candle []string `json:"candle,omitempty"` // [0]timestamp, [1]open, [2]high, [3]low, [4]close, [5]volume, [6]currencyVolume + Table string `json:"table"` + Data []struct { + Candle []string `json:"candle"` // [0]timestamp, [1]open, [2]high, [3]low, [4]close, [5]volume, [6]currencyVolume + InstrumentID string `json:"instrument_id"` + } `json:"data"` } -// WebsocketFundingFeeResponse contains formatted data for funding fee related websocket responses -type WebsocketFundingFeeResponse struct { - FundingRate float64 `json:"funding_rate,string,omitempty"` - FundingTime time.Time `json:"funding_time,omitempty"` - InterestRate float64 `json:"interest_rate,string,omitempty"` -} - -// WebsocketMarkPriceResponse contains formatted data for mark prices -type WebsocketMarkPriceResponse struct { - MarkPrice float64 `json:"mark_price,string,omitempty"` -} - -// WebsocketEstimatedPriceResponse contains formatted data for estimated prices -type WebsocketEstimatedPriceResponse struct { - SettlementPrice float64 `json:"settlement_price,string,omitempty"` -} - -// WebsocketPriceRangeResponse contains formatted data for mark prices -type WebsocketPriceRangeResponse struct { - Highest float64 `json:"highest,omitempty"` - Lowest float64 `json:"lowest,omitempty"` -} - -// WebsocketOrderBooksData contains orderbook data from WebsocketOrderBooksResponse +// WebsocketOrderBooksData is the full websocket response containing orderbook data type WebsocketOrderBooksData struct { - Asks [][]interface{} `json:"asks,omitempty"` // [0] Price, [1] Size, [2] Number of orders - Bids [][]interface{} `json:"bids,omitempty"` // [0] Price, [1] Size, [2] Number of orders - Checksum int32 `json:"checksum,omitempty"` + Table string `json:"table"` + Action string `json:"action"` + Data []WebsocketOrderBook `json:"data"` +} + +// WebsocketOrderBook holds orderbook data +type WebsocketOrderBook struct { + Checksum int32 `json:"checksum,omitempty"` + InstrumentID string `json:"instrument_id"` + Timestamp time.Time `json:"timestamp,omitempty"` + Asks [][]interface{} `json:"asks,omitempty"` // [0] Price, [1] Size, [2] Number of orders + Bids [][]interface{} `json:"bids,omitempty"` // [0] Price, [1] Size, [2] Number of orders } // WebsocketUserSwapPositionResponse contains formatted data for user position data type WebsocketUserSwapPositionResponse struct { - Holding []WebsocketUserSwapPositionHoldingData `json:"holding,omitempty"` + InstrumentID string `json:"instrument_id"` + Timestamp time.Time `json:"timestamp,omitempty"` + Holding []WebsocketUserSwapPositionHoldingData `json:"holding,omitempty"` } // WebsocketUserSwapPositionHoldingData contains formatted data for user position holding data @@ -1409,110 +1390,30 @@ type WebsocketUserSwapPositionHoldingData struct { Timestamp time.Time `json:"timestamp,omitempty"` } -// WebsocketUserSwapFutureAccountResponse contains formatted data for user account data -type WebsocketUserSwapFutureAccountResponse struct { - Equity float64 `json:"equity,string,omitempty"` - FixedBalance float64 `json:"fixed_balance,string,omitempty"` - MarginFrozen float64 `json:"margin_frozen,string,omitempty"` - MarginRatio float64 `json:"margin_ratio,string,omitempty"` - RealizedPnl float64 `json:"realized_pnl,string,omitempty"` - UnrealizedPnl float64 `json:"unrealized_pnl,string,omitempty"` - // MarginMode A member, but part already exists as part of WebsocketDataResponse - // TotalAvailBalance A member, but part already exists as part of WebsocketDataResponse - // Margin A member, but part already exists as part of WebsocketDataResponse -} - -// WebsocketUserSpotAccountResponse contains formatted data for user account data -type WebsocketUserSpotAccountResponse struct { - Balance string `json:"balance"` - Available string `json:"available"` - Currency string `json:"currency"` - ID int64 `json:"id"` - Hold string `json:"hold"` -} - -// WebsocketSpotMarginOrderResponse contains formatted data for user account data -type WebsocketSpotMarginOrderResponse struct { - MarginMode string `json:"margin_mode"` - TotalAvailBalance string `json:"total_avail_balance"` - // UnrealizedPnl A member, but part already exists as part of WebsocketDataResponse - // Equity A member, but part already exists as part of WebsocketDataResponse - // FixedBalance A member, but part already exists as part of WebsocketDataResponse - // InstrumentID A member, but part already exists as part of WebsocketDataResponse - // Margin A member, but part already exists as part of WebsocketDataResponse - // MarginFrozen A member, but part already exists as part of WebsocketDataResponse - // MarginRatio A member, but part already exists as part of WebsocketDataResponse - // RealizedPnl A member, but part already exists as part of WebsocketDataResponse - // Timestamp A member, but part already exists as part of WebsocketDataResponse -} - -// WebsocketUserFutureFixedMarginAccountResponse contains formatted data for user account data -type WebsocketUserFutureFixedMarginAccountResponse map[string]WebsocketUserFutureFixedMarginAccountData - -// WebsocketUserFutureFixedMarginAccountData contains the user fixed margin account data -type WebsocketUserFutureFixedMarginAccountData struct { - Contracts []WebsocketUserSwapFutureAccountResponse `json:"contracts"` - Equity string `json:"equity"` - MarginMode string `json:"margin_mode"` - TotalAvailBalance string `json:"total_avail_balance"` -} - -// WebsocketUserSwapOrdersResponse contains formatted data for user order data -type WebsocketUserSwapOrdersResponse struct { - FilledQuantity float64 `json:"filled_qty,string,omitempty"` - ClientOID string `json:"client_oid,omitempty"` - Fee float64 `json:"fee,string,omitempty"` - ContractValue float64 `json:"contract_val,string,omitempty"` - PriceAverage float64 `json:"price_avg,string,omitempty"` - OrderID string `json:"order_id,omitempty"` - // Size A member, but part already exists as part of WebsocketDataResponse - // Status A member, but part already exists as part of WebsocketDataResponse - // Leverage A member, but part already exists as part of WebsocketDataResponse - // Price A member, but part already exists as part of WebsocketDataResponse - // Type A member, but part already exists as part of WebsocketDataResponse -} - -// WebsocketUserFuturePositionResponse contains formatted data for futures positions data -type WebsocketUserFuturePositionResponse struct { - LongQty string `json:"long_qty"` - LongAvailQty int64 `json:"long_avail_qty"` - LongAvgCost string `json:"long_avg_cost"` - LongSettlementPrice string `json:"long_settlement_price"` - RealisedPnl string `json:"realised_pnl"` - ShortQty string `json:"short_qty"` - ShortAvailQty string `json:"short_avail_qty"` - ShortAvgCost string `json:"short_avg_cost"` - ShortSettlementPrice string `json:"short_settlement_price"` - LiquidationPrice string `json:"liquidation_price"` - Leverage string `json:"leverage"` - CreatedAt string `json:"created_at"` - UpdatedAt string `json:"updated_at"` - LongMargin string `json:"long_margin"` - LongLiquiPrice string `json:"long_liqui_price"` - LongPnlRatio string `json:"long_pnl_ratio"` - ShortMargin string `json:"short_margin"` - ShortLiquiPrice string `json:"short_liqui_price"` - ShortPnlRatio string `json:"short_pnl_ratio"` - LongLeverage string `json:"long_leverage"` - ShortLeverage string `json:"short_leverage"` - // UpdatedAt A member, but part already exists as part of WebsocketDataResponse - // MarginMode A member, but part already exists as part of WebsocketDataResponse - // InstrumentID A member, but part already exists as part of WebsocketDataResponse -} - // WebsocketSpotOrderResponse contains formatted data for spot user orders type WebsocketSpotOrderResponse struct { - FilledNotional float64 `json:"filled_notional,string"` - FilledSize float64 `json:"filled_size,string"` - Notional float64 `json:"notional,string"` - Size float64 `json:"size,string"` - Status string `json:"status"` - MarginTrading int64 `json:"margin_trading,omitempty"` - Type string `json:"type"` - // Price A member, but part already exists as part of WebsocketDataResponse - // InstrumentID A member, but part already exists as part of WebsocketDataResponse - // Timestamp A member, but part already exists as part of WebsocketDataResponse - // OrderID A member, but part already exists as part of WebsocketDataResponse + Table string `json:"table"` + Data []struct { + ClientOid string `json:"client_oid"` + CreatedAt time.Time `json:"created_at"` + FilledNotional float64 `json:"filled_notional,string"` + FilledSize float64 `json:"filled_size,string"` + InstrumentID string `json:"instrument_id"` + LastFillPx float64 `json:"last_fill_px,string"` + LastFillQty float64 `json:"last_fill_qty,string"` + LastFillTime time.Time `json:"last_fill_time"` + MarginTrading int64 `json:"margin_trading,string"` + Notional string `json:"notional"` + OrderID string `json:"order_id"` + OrderType int64 `json:"order_type,string"` + Price float64 `json:"price,string"` + Side string `json:"side"` + Size float64 `json:"size,string"` + State int64 `json:"state,string"` + Status string `json:"status"` + Timestamp time.Time `json:"timestamp"` + Type string `json:"type"` + } `json:"data"` } // WebsocketErrorResponse yo diff --git a/exchanges/okgroup/okgroup_websocket.go b/exchanges/okgroup/okgroup_websocket.go index bd67b3cf..e2a69298 100644 --- a/exchanges/okgroup/okgroup_websocket.go +++ b/exchanges/okgroup/okgroup_websocket.go @@ -16,6 +16,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" @@ -188,7 +189,7 @@ func (o *OKGroup) WsConnect() error { } wg := sync.WaitGroup{} wg.Add(1) - go o.WsHandleData(&wg) + go o.WsReadData(&wg) if o.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) { err = o.WsLogin() if err != nil { @@ -205,66 +206,6 @@ func (o *OKGroup) WsConnect() error { return nil } -// WsHandleData handles the read data from the websocket connection -func (o *OKGroup) WsHandleData(wg *sync.WaitGroup) { - o.Websocket.Wg.Add(1) - defer func() { - o.Websocket.Wg.Done() - }() - - wg.Done() - - for { - select { - case <-o.Websocket.ShutdownC: - return - - default: - resp, err := o.WebsocketConn.ReadMessage() - if err != nil { - o.Websocket.ReadMessageErrors <- err - return - } - o.Websocket.TrafficAlert <- struct{}{} - var dataResponse WebsocketDataResponse - err = json.Unmarshal(resp.Raw, &dataResponse) - if err == nil && dataResponse.Table != "" { - if len(dataResponse.Data) > 0 { - o.WsHandleDataResponse(&dataResponse) - } - continue - } - var errorResponse WebsocketErrorResponse - err = json.Unmarshal(resp.Raw, &errorResponse) - if err == nil && errorResponse.ErrorCode > 0 { - if o.Verbose { - log.Debugf(log.ExchangeSys, - "WS Error Event: %v Message: %v for %s", - errorResponse.Event, - errorResponse.Message, - o.Name) - } - o.WsHandleErrorResponse(errorResponse) - continue - } - var eventResponse WebsocketEventResponse - err = json.Unmarshal(resp.Raw, &eventResponse) - if err == nil && eventResponse.Event != "" { - if eventResponse.Event == "login" { - o.Websocket.SetCanUseAuthenticatedEndpoints(eventResponse.Success) - } - if o.Verbose { - log.Debugf(log.ExchangeSys, - "WS Event: %v on Channel: %v for %s", - eventResponse.Event, - eventResponse.Channel, - o.Name) - } - } - } - } -} - // WsLogin sends a login request to websocket to enable access to authenticated endpoints func (o *OKGroup) WsLogin() error { o.Websocket.SetCanUseAuthenticatedEndpoints(true) @@ -292,113 +233,166 @@ func (o *OKGroup) WsLogin() error { return nil } -// WsHandleErrorResponse sends an error message to ws handler -func (o *OKGroup) WsHandleErrorResponse(event WebsocketErrorResponse) { - errorMessage := fmt.Sprintf("%v error - %v message: %s ", - o.Name, - event.ErrorCode, - event.Message) - if o.Verbose { - log.Error(log.ExchangeSys, errorMessage) - } - o.Websocket.DataHandler <- fmt.Errorf(errorMessage) -} +// WsReadData receives and passes on websocket messages for processing +func (o *OKGroup) WsReadData(wg *sync.WaitGroup) { + o.Websocket.Wg.Add(1) + defer func() { + o.Websocket.Wg.Done() + }() + wg.Done() -// GetWsChannelWithoutOrderType takes WebsocketDataResponse.Table and returns -// The base channel name eg receive "spot/depth5:BTC-USDT" return "depth5" -func (o *OKGroup) GetWsChannelWithoutOrderType(table string) string { - index := strings.Index(table, "/") - if index == -1 { - return table - } - channel := table[index+1:] - index = strings.Index(channel, ":") - // Some events do not contain a currency - if index == -1 { - return channel - } - - return channel[:index] -} - -// GetAssetTypeFromTableName gets the asset type from the table name -// eg "spot/ticker:BTCUSD" results in "SPOT" -func (o *OKGroup) GetAssetTypeFromTableName(table string) asset.Item { - assetIndex := strings.Index(table, "/") - 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: - o.wsProcessCandles(response) - case okGroupWsDepth, okGroupWsDepth5: - // Locking, orderbooks cannot be processed out of order - orderbookMutex.Lock() - err := o.WsProcessOrderBook(response) - if err != nil { - 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) + for { + select { + case <-o.Websocket.ShutdownC: + return + default: + resp, err := o.WebsocketConn.ReadMessage() + if err != nil { + o.Websocket.ReadMessageErrors <- err + return + } + o.Websocket.TrafficAlert <- struct{}{} + err = o.WsHandleData(resp.Raw) + if err != nil { + o.Websocket.DataHandler <- err } } - orderbookMutex.Unlock() - case okGroupWsTicker: - o.wsProcessTickers(response) - case okGroupWsTrade: - o.wsProcessTrades(response) - default: - 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, exchangeName string) { - for i := range response.Data { - log.Warnf(log.ExchangeSys, - "%s Unhandled channel: '%v'. Instrument '%v' Timestamp '%v'", - exchangeName, - response.Table, - response.Data[i].InstrumentID, - response.Data[i].Timestamp) +// WsHandleData will read websocket raw data and pass to appropriate handler +func (o *OKGroup) WsHandleData(respRaw []byte) error { + var dataResponse WebsocketDataResponse + err := json.Unmarshal(respRaw, &dataResponse) + if err != nil { + return err } + if len(dataResponse.Data) > 0 { + switch o.GetWsChannelWithoutOrderType(dataResponse.Table) { + case okGroupWsCandle60s, okGroupWsCandle180s, okGroupWsCandle300s, + okGroupWsCandle900s, okGroupWsCandle1800s, okGroupWsCandle3600s, + okGroupWsCandle7200s, okGroupWsCandle14400s, okGroupWsCandle21600s, + okGroupWsCandle43200s, okGroupWsCandle86400s, okGroupWsCandle604900s: + return o.wsProcessCandles(respRaw) + case okGroupWsDepth, okGroupWsDepth5: + return o.WsProcessOrderBook(respRaw) + case okGroupWsTicker: + return o.wsProcessTickers(respRaw) + case okGroupWsTrade: + return o.wsProcessTrades(respRaw) + case okGroupWsOrder: + return o.wsProcessOrder(respRaw) + } + o.Websocket.DataHandler <- wshandler.UnhandledMessageWarning{Message: o.Name + wshandler.UnhandledMessage + string(respRaw)} + return nil + } + + var errorResponse WebsocketErrorResponse + err = json.Unmarshal(respRaw, &errorResponse) + if err == nil && errorResponse.ErrorCode > 0 { + return fmt.Errorf("%v error - %v message: %s ", + o.Name, + errorResponse.ErrorCode, + errorResponse.Message) + } + var eventResponse WebsocketEventResponse + err = json.Unmarshal(respRaw, &eventResponse) + if err == nil && eventResponse.Event != "" { + if eventResponse.Event == "login" { + o.Websocket.SetCanUseAuthenticatedEndpoints(eventResponse.Success) + } + if o.Verbose { + log.Debug(log.ExchangeSys, + o.Name+" - "+eventResponse.Event+" on channel: "+eventResponse.Channel) + } + } + return nil +} + +// StringToOrderStatus converts order status IDs to internal types +func StringToOrderStatus(num int64) (order.Status, error) { + switch num { + case -2: + return order.Rejected, nil + case -1: + return order.Cancelled, nil + case 0: + return order.Active, nil + case 1: + return order.PartiallyFilled, nil + case 2: + return order.Filled, nil + case 3: + return order.New, nil + case 4: + return order.PendingCancel, nil + default: + return order.UnknownStatus, fmt.Errorf("%v not recognised as order status", num) + } +} + +func (o *OKGroup) wsProcessOrder(respRaw []byte) error { + var resp WebsocketSpotOrderResponse + err := json.Unmarshal(respRaw, &resp) + if err != nil { + return err + } + for i := range resp.Data { + var oType order.Type + var oSide order.Side + var oStatus order.Status + oType, err = order.StringToOrderType(resp.Data[i].Type) + if err != nil { + o.Websocket.DataHandler <- order.ClassificationError{ + Exchange: o.Name, + OrderID: resp.Data[i].OrderID, + Err: err, + } + } + oSide, err = order.StringToOrderSide(resp.Data[i].Side) + if err != nil { + o.Websocket.DataHandler <- order.ClassificationError{ + Exchange: o.Name, + OrderID: resp.Data[i].OrderID, + Err: err, + } + } + oStatus, err = StringToOrderStatus(resp.Data[i].State) + if err != nil { + o.Websocket.DataHandler <- order.ClassificationError{ + Exchange: o.Name, + OrderID: resp.Data[i].OrderID, + Err: err, + } + } + o.Websocket.DataHandler <- &order.Detail{ + ImmediateOrCancel: resp.Data[i].OrderType == 3, + FillOrKill: resp.Data[i].OrderType == 2, + PostOnly: resp.Data[i].OrderType == 1, + Price: resp.Data[i].Price, + Amount: resp.Data[i].Size, + ExecutedAmount: resp.Data[i].LastFillQty, + RemainingAmount: resp.Data[i].Size - resp.Data[i].LastFillQty, + Exchange: o.Name, + ID: resp.Data[i].OrderID, + Type: oType, + Side: oSide, + Status: oStatus, + AssetType: o.GetAssetTypeFromTableName(resp.Table), + Date: resp.Data[i].CreatedAt, + Pair: currency.NewPairFromString(resp.Data[i].InstrumentID), + } + } + return nil } // wsProcessTickers converts ticker data and sends it to the datahandler -func (o *OKGroup) wsProcessTickers(response *WebsocketDataResponse) { +func (o *OKGroup) wsProcessTickers(respRaw []byte) error { + var response WebsocketTickerData + err := json.Unmarshal(respRaw, &response) + if err != nil { + return err + } for i := range response.Data { a := o.GetAssetTypeFromTableName(response.Table) var c currency.Pair @@ -410,7 +404,6 @@ func (o *OKGroup) wsProcessTickers(response *WebsocketDataResponse) { f := strings.Split(response.Data[i].InstrumentID, delimiterDash) c = currency.NewPairWithDelimiter(f[0], f[1], delimiterDash) } - o.Websocket.DataHandler <- &ticker.Price{ ExchangeName: o.Name, Open: response.Data[i].Open24h, @@ -422,15 +415,21 @@ func (o *OKGroup) wsProcessTickers(response *WebsocketDataResponse) { Bid: response.Data[i].BestBid, Ask: response.Data[i].BestAsk, Last: response.Data[i].Last, - LastUpdated: response.Data[i].Timestamp, AssetType: o.GetAssetTypeFromTableName(response.Table), Pair: c, + LastUpdated: response.Data[i].Timestamp, } } + return nil } // wsProcessTrades converts trade data and sends it to the datahandler -func (o *OKGroup) wsProcessTrades(response *WebsocketDataResponse) { +func (o *OKGroup) wsProcessTrades(respRaw []byte) error { + var response WebsocketTradeResponse + err := json.Unmarshal(respRaw, &response) + if err != nil { + return err + } for i := range response.Data { a := o.GetAssetTypeFromTableName(response.Table) var c currency.Pair @@ -442,21 +441,33 @@ func (o *OKGroup) wsProcessTrades(response *WebsocketDataResponse) { f := strings.Split(response.Data[i].InstrumentID, delimiterDash) c = currency.NewPairWithDelimiter(f[0], f[1], delimiterDash) } - + tSide, err := order.StringToOrderSide(response.Data[i].Side) + if err != nil { + o.Websocket.DataHandler <- order.ClassificationError{ + Exchange: o.Name, + Err: err, + } + } o.Websocket.DataHandler <- wshandler.TradeData{ Amount: response.Data[i].Size, AssetType: o.GetAssetTypeFromTableName(response.Table), CurrencyPair: c, Exchange: o.Name, - Price: response.Data[i].WebsocketTradeResponse.Price, - Side: response.Data[i].Side, + Price: response.Data[i].Price, + Side: tSide, Timestamp: response.Data[i].Timestamp, } } + return nil } // wsProcessCandles converts candle data and sends it to the data handler -func (o *OKGroup) wsProcessCandles(response *WebsocketDataResponse) { +func (o *OKGroup) wsProcessCandles(respRaw []byte) error { + var response WebsocketCandleResponse + err := json.Unmarshal(respRaw, &response) + if err != nil { + return err + } for i := range response.Data { a := o.GetAssetTypeFromTableName(response.Table) var c currency.Pair @@ -470,10 +481,9 @@ func (o *OKGroup) wsProcessCandles(response *WebsocketDataResponse) { } timeData, err := time.Parse(time.RFC3339Nano, - response.Data[i].WebsocketCandleResponse.Candle[0]) + response.Data[i].Candle[0]) if err != nil { - log.Errorf(log.ExchangeSys, - "%v Time data could not be parsed: %v", + return fmt.Errorf("%v Time data could not be parsed: %v", o.Name, response.Data[i].Candle[0]) } @@ -494,36 +504,39 @@ func (o *OKGroup) wsProcessCandles(response *WebsocketDataResponse) { } klineData.OpenPrice, err = strconv.ParseFloat(response.Data[i].Candle[1], 64) if err != nil { - o.Websocket.DataHandler <- err - continue + return err } klineData.HighPrice, err = strconv.ParseFloat(response.Data[i].Candle[2], 64) if err != nil { - o.Websocket.DataHandler <- err - continue + return err } klineData.LowPrice, err = strconv.ParseFloat(response.Data[i].Candle[3], 64) if err != nil { - o.Websocket.DataHandler <- err - continue + return err } klineData.ClosePrice, err = strconv.ParseFloat(response.Data[i].Candle[4], 64) if err != nil { - o.Websocket.DataHandler <- err - continue + return err } klineData.Volume, err = strconv.ParseFloat(response.Data[i].Candle[5], 64) if err != nil { - o.Websocket.DataHandler <- err - continue + return err } o.Websocket.DataHandler <- klineData } + return nil } // WsProcessOrderBook Validates the checksum and updates internal orderbook values -func (o *OKGroup) WsProcessOrderBook(response *WebsocketDataResponse) (err error) { +func (o *OKGroup) WsProcessOrderBook(respRaw []byte) error { + var response WebsocketOrderBooksData + err := json.Unmarshal(respRaw, &response) + if err != nil { + return err + } + orderbookMutex.Lock() + defer orderbookMutex.Unlock() for i := range response.Data { a := o.GetAssetTypeFromTableName(response.Table) var c currency.Pair @@ -537,21 +550,44 @@ func (o *OKGroup) WsProcessOrderBook(response *WebsocketDataResponse) (err error } if response.Action == okGroupWsOrderbookPartial { - err = o.WsProcessPartialOrderBook(&response.Data[i], c, a) + err := o.WsProcessPartialOrderBook(&response.Data[i], c, a) if err != nil { - return + o.wsResubscribeToOrderbook(&response) + return err } } else if response.Action == okGroupWsOrderbookUpdate { if len(response.Data[i].Asks) == 0 && len(response.Data[i].Bids) == 0 { - continue + return nil } - err = o.WsProcessUpdateOrderbook(&response.Data[i], c, a) + err := o.WsProcessUpdateOrderbook(&response.Data[i], c, a) if err != nil { - return + o.wsResubscribeToOrderbook(&response) + return err } } } - return + return nil +} + +func (o *OKGroup) wsResubscribeToOrderbook(response *WebsocketOrderBooksData) { + 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) + } } // AppendWsOrderbookItems adds websocket orderbook data bid/asks into an orderbook item array @@ -573,7 +609,7 @@ func (o *OKGroup) AppendWsOrderbookItems(entries [][]interface{}) ([]orderbook.I // 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, a asset.Item) error { +func (o *OKGroup) WsProcessPartialOrderBook(wsEventData *WebsocketOrderBook, instrument currency.Pair, a asset.Item) error { signedChecksum := o.CalculatePartialOrderbookChecksum(wsEventData) if signedChecksum != wsEventData.Checksum { return fmt.Errorf("%s channel: %s. Orderbook partial for %v checksum invalid", @@ -622,7 +658,7 @@ 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, a asset.Item) error { +func (o *OKGroup) WsProcessUpdateOrderbook(wsEventData *WebsocketOrderBook, instrument currency.Pair, a asset.Item) error { update := wsorderbook.WebsocketOrderbookUpdate{ Asset: a, Pair: instrument, @@ -669,7 +705,7 @@ func (o *OKGroup) WsProcessUpdateOrderbook(wsEventData *WebsocketDataWrapper, in // 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 { +func (o *OKGroup) CalculatePartialOrderbookChecksum(orderbookData *WebsocketOrderBook) int32 { var checksum strings.Builder for i := 0; i < allowableIterations; i++ { if len(orderbookData.Bids)-1 >= i { @@ -840,3 +876,41 @@ func (o *OKGroup) Unsubscribe(channelToSubscribe wshandler.WebsocketChannelSubsc } return o.WebsocketConn.SendJSONMessage(request) } + +// GetWsChannelWithoutOrderType takes WebsocketDataResponse.Table and returns +// The base channel name eg receive "spot/depth5:BTC-USDT" return "depth5" +func (o *OKGroup) GetWsChannelWithoutOrderType(table string) string { + index := strings.Index(table, "/") + if index == -1 { + return table + } + channel := table[index+1:] + index = strings.Index(channel, ":") + // Some events do not contain a currency + if index == -1 { + return channel + } + + return channel[:index] +} + +// GetAssetTypeFromTableName gets the asset type from the table name +// eg "spot/ticker:BTCUSD" results in "SPOT" +func (o *OKGroup) GetAssetTypeFromTableName(table string) asset.Item { + assetIndex := strings.Index(table, "/") + 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]) + } +} diff --git a/exchanges/okgroup/okgroup_wrapper.go b/exchanges/okgroup/okgroup_wrapper.go index 32c20a53..fe9ba7ab 100644 --- a/exchanges/okgroup/okgroup_wrapper.go +++ b/exchanges/okgroup/okgroup_wrapper.go @@ -276,11 +276,11 @@ func (o *OKGroup) SubmitOrder(s *order.Submit) (resp order.SubmitResponse, err e request := PlaceOrderRequest{ ClientOID: s.ClientID, InstrumentID: o.FormatExchangeCurrency(s.Pair, asset.Spot).String(), - Side: s.OrderSide.Lower(), - Type: s.OrderType.Lower(), + Side: s.Side.Lower(), + Type: s.Type.Lower(), Size: strconv.FormatFloat(s.Amount, 'f', -1, 64), } - if s.OrderType == order.Limit { + if s.Type == order.Limit { request.Price = strconv.FormatFloat(s.Price, 'f', -1, 64) } @@ -291,7 +291,7 @@ func (o *OKGroup) SubmitOrder(s *order.Submit) (resp order.SubmitResponse, err e resp.IsOrderPlaced = orderResponse.Result resp.OrderID = orderResponse.OrderID - if s.OrderType == order.Market { + if s.Type == order.Market { resp.FullyMatched = true } return @@ -305,12 +305,12 @@ func (o *OKGroup) ModifyOrder(action *order.Modify) (string, error) { // CancelOrder cancels an order by its corresponding ID number func (o *OKGroup) CancelOrder(orderCancellation *order.Cancel) (err error) { - orderID, err := strconv.ParseInt(orderCancellation.OrderID, 10, 64) + orderID, err := strconv.ParseInt(orderCancellation.ID, 10, 64) if err != nil { return } orderCancellationResponse, err := o.CancelSpotOrder(CancelSpotOrderRequest{ - InstrumentID: o.FormatExchangeCurrency(orderCancellation.CurrencyPair, + InstrumentID: o.FormatExchangeCurrency(orderCancellation.Pair, asset.Spot).String(), OrderID: orderID, }) @@ -324,7 +324,7 @@ func (o *OKGroup) CancelOrder(orderCancellation *order.Cancel) (err error) { // CancelAllOrders cancels all orders associated with a currency pair func (o *OKGroup) CancelAllOrders(orderCancellation *order.Cancel) (resp order.CancelAllResponse, err error) { - orderIDs := strings.Split(orderCancellation.OrderID, ",") + orderIDs := strings.Split(orderCancellation.ID, ",") resp.Status = make(map[string]string) var orderIDNumbers []int64 for i := range orderIDs { @@ -337,7 +337,7 @@ func (o *OKGroup) CancelAllOrders(orderCancellation *order.Cancel) (resp order.C } cancelOrdersResponse, err := o.CancelMultipleSpotOrders(CancelMultipleSpotOrdersRequest{ - InstrumentID: o.FormatExchangeCurrency(orderCancellation.CurrencyPair, + InstrumentID: o.FormatExchangeCurrency(orderCancellation.Pair, asset.Spot).String(), OrderIDs: orderIDNumbers, }) @@ -362,13 +362,13 @@ func (o *OKGroup) GetOrderInfo(orderID string) (resp order.Detail, err error) { } resp = order.Detail{ Amount: mOrder.Size, - CurrencyPair: currency.NewPairDelimiter(mOrder.InstrumentID, + Pair: currency.NewPairDelimiter(mOrder.InstrumentID, o.GetPairFormat(asset.Spot, false).Delimiter), Exchange: o.Name, - OrderDate: mOrder.Timestamp, + Date: mOrder.Timestamp, ExecutedAmount: mOrder.FilledSize, Status: order.Status(mOrder.Status), - OrderSide: order.Side(mOrder.Side), + Side: order.Side(mOrder.Side), } return } @@ -422,9 +422,9 @@ func (o *OKGroup) WithdrawFiatFundsToInternationalBank(withdrawRequest *withdraw // GetActiveOrders retrieves any orders that are active/open func (o *OKGroup) GetActiveOrders(req *order.GetOrdersRequest) (resp []order.Detail, err error) { - for x := range req.Currencies { + for x := range req.Pairs { spotOpenOrders, err := o.GetSpotOpenOrders(GetSpotOpenOrdersRequest{ - InstrumentID: o.FormatExchangeCurrency(req.Currencies[x], + InstrumentID: o.FormatExchangeCurrency(req.Pairs[x], asset.Spot).String(), }) if err != nil { @@ -435,12 +435,12 @@ func (o *OKGroup) GetActiveOrders(req *order.GetOrdersRequest) (resp []order.Det ID: spotOpenOrders[i].OrderID, Price: spotOpenOrders[i].Price, Amount: spotOpenOrders[i].Size, - CurrencyPair: req.Currencies[x], + Pair: req.Pairs[x], Exchange: o.Name, - OrderSide: order.Side(spotOpenOrders[i].Side), - OrderType: order.Type(spotOpenOrders[i].Type), + Side: order.Side(spotOpenOrders[i].Side), + Type: order.Type(spotOpenOrders[i].Type), ExecutedAmount: spotOpenOrders[i].FilledSize, - OrderDate: spotOpenOrders[i].Timestamp, + Date: spotOpenOrders[i].Timestamp, Status: order.Status(spotOpenOrders[i].Status), }) } @@ -452,10 +452,10 @@ func (o *OKGroup) GetActiveOrders(req *order.GetOrdersRequest) (resp []order.Det // GetOrderHistory retrieves account order information // Can Limit response to specific order status func (o *OKGroup) GetOrderHistory(req *order.GetOrdersRequest) (resp []order.Detail, err error) { - for x := range req.Currencies { + for x := range req.Pairs { spotOpenOrders, err := o.GetSpotOrders(GetSpotOrdersRequest{ Status: strings.Join([]string{"filled", "cancelled", "failure"}, "|"), - InstrumentID: o.FormatExchangeCurrency(req.Currencies[x], + InstrumentID: o.FormatExchangeCurrency(req.Pairs[x], asset.Spot).String(), }) if err != nil { @@ -466,12 +466,12 @@ func (o *OKGroup) GetOrderHistory(req *order.GetOrdersRequest) (resp []order.Det ID: spotOpenOrders[i].OrderID, Price: spotOpenOrders[i].Price, Amount: spotOpenOrders[i].Size, - CurrencyPair: req.Currencies[x], + Pair: req.Pairs[x], Exchange: o.Name, - OrderSide: order.Side(spotOpenOrders[i].Side), - OrderType: order.Type(spotOpenOrders[i].Type), + Side: order.Side(spotOpenOrders[i].Side), + Type: order.Type(spotOpenOrders[i].Type), ExecutedAmount: spotOpenOrders[i].FilledSize, - OrderDate: spotOpenOrders[i].Timestamp, + Date: spotOpenOrders[i].Timestamp, Status: order.Status(spotOpenOrders[i].Status), }) } diff --git a/exchanges/order/order_test.go b/exchanges/order/order_test.go index 86d0b7e7..7971c9e2 100644 --- a/exchanges/order/order_test.go +++ b/exchanges/order/order_test.go @@ -71,11 +71,11 @@ func TestValidate(t *testing.T) { for x := range tester { s := Submit{ - Pair: tester[x].Pair, - OrderSide: tester[x].Side, - OrderType: tester[x].Type, - Amount: tester[x].Amount, - Price: tester[x].Price, + Pair: tester[x].Pair, + Side: tester[x].Side, + Type: tester[x].Type, + Amount: tester[x].Amount, + Price: tester[x].Price, } if err := s.Validate(); err != tester[x].ExpectedErr { t.Errorf("Unexpected result. Got: %s, want: %s", err, tester[x].ExpectedErr) @@ -115,10 +115,10 @@ func TestFilterOrdersByType(t *testing.T) { var orders = []Detail{ { - OrderType: ImmediateOrCancel, + Type: ImmediateOrCancel, }, { - OrderType: Limit, + Type: Limit, }, } @@ -143,10 +143,10 @@ func TestFilterOrdersBySide(t *testing.T) { var orders = []Detail{ { - OrderSide: Buy, + Side: Buy, }, { - OrderSide: Sell, + Side: Sell, }, {}, } @@ -172,13 +172,13 @@ func TestFilterOrdersByTickRange(t *testing.T) { var orders = []Detail{ { - OrderDate: time.Unix(100, 0), + Date: time.Unix(100, 0), }, { - OrderDate: time.Unix(110, 0), + Date: time.Unix(110, 0), }, { - OrderDate: time.Unix(111, 0), + Date: time.Unix(111, 0), }, } @@ -208,13 +208,13 @@ func TestFilterOrdersByCurrencies(t *testing.T) { var orders = []Detail{ { - CurrencyPair: currency.NewPair(currency.BTC, currency.USD), + Pair: currency.NewPair(currency.BTC, currency.USD), }, { - CurrencyPair: currency.NewPair(currency.LTC, currency.EUR), + Pair: currency.NewPair(currency.LTC, currency.EUR), }, { - CurrencyPair: currency.NewPair(currency.DOGE, currency.RUB), + Pair: currency.NewPair(currency.DOGE, currency.RUB), }, } @@ -275,26 +275,26 @@ func TestSortOrdersByDate(t *testing.T) { orders := []Detail{ { - OrderDate: time.Unix(0, 0), + Date: time.Unix(0, 0), }, { - OrderDate: time.Unix(1, 0), + Date: time.Unix(1, 0), }, { - OrderDate: time.Unix(2, 0), + Date: time.Unix(2, 0), }, } SortOrdersByDate(&orders, false) - if orders[0].OrderDate.Unix() != time.Unix(0, 0).Unix() { + if orders[0].Date.Unix() != time.Unix(0, 0).Unix() { t.Errorf("Expected: '%v', received: '%v'", time.Unix(0, 0).Unix(), - orders[0].OrderDate.Unix()) + orders[0].Date.Unix()) } SortOrdersByDate(&orders, true) - if orders[0].OrderDate.Unix() != time.Unix(2, 0).Unix() { + if orders[0].Date.Unix() != time.Unix(2, 0).Unix() { t.Errorf("Expected: '%v', received: '%v'", time.Unix(2, 0).Unix(), - orders[0].OrderDate.Unix()) + orders[0].Date.Unix()) } } @@ -303,40 +303,40 @@ func TestSortOrdersByCurrency(t *testing.T) { orders := []Detail{ { - CurrencyPair: currency.NewPairWithDelimiter(currency.BTC.String(), + Pair: currency.NewPairWithDelimiter(currency.BTC.String(), currency.USD.String(), "-"), }, { - CurrencyPair: currency.NewPairWithDelimiter(currency.DOGE.String(), + Pair: currency.NewPairWithDelimiter(currency.DOGE.String(), currency.USD.String(), "-"), }, { - CurrencyPair: currency.NewPairWithDelimiter(currency.BTC.String(), + Pair: currency.NewPairWithDelimiter(currency.BTC.String(), currency.RUB.String(), "-"), }, { - CurrencyPair: currency.NewPairWithDelimiter(currency.LTC.String(), + Pair: currency.NewPairWithDelimiter(currency.LTC.String(), currency.EUR.String(), "-"), }, { - CurrencyPair: currency.NewPairWithDelimiter(currency.LTC.String(), + Pair: currency.NewPairWithDelimiter(currency.LTC.String(), currency.AUD.String(), "-"), }, } SortOrdersByCurrency(&orders, false) - if orders[0].CurrencyPair.String() != currency.BTC.String()+"-"+currency.RUB.String() { + if orders[0].Pair.String() != currency.BTC.String()+"-"+currency.RUB.String() { t.Errorf("Expected: '%v', received: '%v'", currency.BTC.String()+"-"+currency.RUB.String(), - orders[0].CurrencyPair.String()) + orders[0].Pair.String()) } SortOrdersByCurrency(&orders, true) - if orders[0].CurrencyPair.String() != currency.LTC.String()+"-"+currency.EUR.String() { + if orders[0].Pair.String() != currency.LTC.String()+"-"+currency.EUR.String() { t.Errorf("Expected: '%v', received: '%v'", currency.LTC.String()+"-"+currency.EUR.String(), - orders[0].CurrencyPair.String()) + orders[0].Pair.String()) } } @@ -345,28 +345,28 @@ func TestSortOrdersByOrderSide(t *testing.T) { orders := []Detail{ { - OrderSide: Buy, + Side: Buy, }, { - OrderSide: Sell, + Side: Sell, }, { - OrderSide: Sell, + Side: Sell, }, { - OrderSide: Buy, + Side: Buy, }, } SortOrdersBySide(&orders, false) - if !strings.EqualFold(orders[0].OrderSide.String(), Buy.String()) { + if !strings.EqualFold(orders[0].Side.String(), Buy.String()) { t.Errorf("Expected: '%v', received: '%v'", Buy, - orders[0].OrderSide) + orders[0].Side) } SortOrdersBySide(&orders, true) - if !strings.EqualFold(orders[0].OrderSide.String(), Sell.String()) { + if !strings.EqualFold(orders[0].Side.String(), Sell.String()) { t.Errorf("Expected: '%v', received: '%v'", Sell, - orders[0].OrderSide) + orders[0].Side) } } @@ -375,28 +375,28 @@ func TestSortOrdersByOrderType(t *testing.T) { orders := []Detail{ { - OrderType: Market, + Type: Market, }, { - OrderType: Limit, + Type: Limit, }, { - OrderType: ImmediateOrCancel, + Type: ImmediateOrCancel, }, { - OrderType: TrailingStop, + Type: TrailingStop, }, } SortOrdersByType(&orders, false) - if !strings.EqualFold(orders[0].OrderType.String(), ImmediateOrCancel.String()) { + if !strings.EqualFold(orders[0].Type.String(), ImmediateOrCancel.String()) { t.Errorf("Expected: '%v', received: '%v'", ImmediateOrCancel, - orders[0].OrderType) + orders[0].Type) } SortOrdersByType(&orders, true) - if !strings.EqualFold(orders[0].OrderType.String(), TrailingStop.String()) { + if !strings.EqualFold(orders[0].Type.String(), TrailingStop.String()) { t.Errorf("Expected: '%v', received: '%v'", TrailingStop, - orders[0].OrderType) + orders[0].Type) } } @@ -420,7 +420,7 @@ var stringsToOrderSide = []struct { {"any", AnySide, nil}, {"ANY", AnySide, nil}, {"aNy", AnySide, nil}, - {"woahMan", Buy, errors.New("woahMan not recognised as side type")}, + {"woahMan", Buy, errors.New("woahMan not recognised as order side")}, } func TestStringToOrderSide(t *testing.T) { @@ -453,16 +453,18 @@ var stringsToOrderType = []struct { {"immediate_or_cancel", ImmediateOrCancel, nil}, {"IMMEDIATE_OR_CANCEL", ImmediateOrCancel, nil}, {"iMmEdIaTe_Or_CaNcEl", ImmediateOrCancel, nil}, + {"iMmEdIaTe Or CaNcEl", ImmediateOrCancel, nil}, {"stop", Stop, nil}, {"STOP", Stop, nil}, {"sToP", Stop, nil}, - {"trailingstop", TrailingStop, nil}, - {"TRAILINGSTOP", TrailingStop, nil}, - {"tRaIlInGsToP", TrailingStop, nil}, + {"trailing_stop", TrailingStop, nil}, + {"TRAILING_STOP", TrailingStop, nil}, + {"tRaIlInG_sToP", TrailingStop, nil}, + {"tRaIlInG sToP", TrailingStop, nil}, {"any", AnyType, nil}, {"ANY", AnyType, nil}, {"aNy", AnyType, nil}, - {"woahMan", Unknown, errors.New("woahMan not recognised as order type")}, + {"woahMan", UnknownType, errors.New("woahMan not recognised as order type")}, } func TestStringToOrderType(t *testing.T) { @@ -516,7 +518,13 @@ var stringsToOrderStatus = []struct { {"hidden", Hidden, nil}, {"HIDDEN", Hidden, nil}, {"hIdDeN", Hidden, nil}, - {"woahMan", UnknownStatus, errors.New("woahMan not recognised as order STATUS")}, + {"market_unavailable", MarketUnavailable, nil}, + {"MARKET_UNAVAILABLE", MarketUnavailable, nil}, + {"mArKeT_uNaVaIlAbLe", MarketUnavailable, nil}, + {"insufficient_balance", InsufficientBalance, nil}, + {"INSUFFICIENT_BALANCE", InsufficientBalance, nil}, + {"iNsUfFiCiEnT_bAlAnCe", InsufficientBalance, nil}, + {"woahMan", UnknownStatus, errors.New("woahMan not recognised as order status")}, } func TestStringToOrderStatus(t *testing.T) { @@ -534,3 +542,375 @@ func TestStringToOrderStatus(t *testing.T) { }) } } + +func TestUpdateOrderFromModify(t *testing.T) { + var leet = "1337" + od := Detail{ + ImmediateOrCancel: false, + HiddenOrder: false, + FillOrKill: false, + PostOnly: false, + Leverage: "", + Price: 0, + Amount: 0, + LimitPriceUpper: 0, + LimitPriceLower: 0, + TriggerPrice: 0, + TargetAmount: 0, + ExecutedAmount: 0, + RemainingAmount: 0, + Fee: 0, + Exchange: "", + ID: "1", + AccountID: "", + ClientID: "", + WalletAddress: "", + Type: "", + Side: "", + Status: "", + AssetType: "", + Date: time.Time{}, + LastUpdated: time.Time{}, + Pair: currency.Pair{}, + Trades: nil, + } + updated := time.Now() + om := Modify{ + ImmediateOrCancel: true, + HiddenOrder: true, + FillOrKill: true, + PostOnly: true, + Leverage: "1", + Price: 1, + Amount: 1, + LimitPriceUpper: 1, + LimitPriceLower: 1, + TriggerPrice: 1, + TargetAmount: 1, + ExecutedAmount: 1, + RemainingAmount: 1, + Fee: 1, + Exchange: "1", + InternalOrderID: "1", + ID: "1", + AccountID: "1", + ClientID: "1", + WalletAddress: "1", + Type: "1", + Side: "1", + Status: "1", + AssetType: "1", + LastUpdated: updated, + Pair: currency.NewPairFromString("BTCUSD"), + Trades: []TradeHistory{}, + } + + od.UpdateOrderFromModify(&om) + if od.InternalOrderID == "1" { + t.Error("Should not be able to update the internal order ID") + } + if !od.ImmediateOrCancel { + t.Error("Failed to update") + } + if !od.HiddenOrder { + t.Error("Failed to update") + } + if !od.FillOrKill { + t.Error("Failed to update") + } + if !od.PostOnly { + t.Error("Failed to update") + } + if od.Leverage != "1" { + t.Error("Failed to update") + } + if od.Price != 1 { + t.Error("Failed to update") + } + if od.Amount != 1 { + t.Error("Failed to update") + } + if od.LimitPriceLower != 1 { + t.Error("Failed to update") + } + if od.LimitPriceUpper != 1 { + t.Error("Failed to update") + } + if od.TriggerPrice != 1 { + t.Error("Failed to update") + } + if od.TargetAmount != 1 { + t.Error("Failed to update") + } + if od.ExecutedAmount != 1 { + t.Error("Failed to update") + } + if od.RemainingAmount != 1 { + t.Error("Failed to update") + } + if od.Fee != 1 { + t.Error("Failed to update") + } + if od.Exchange != "" { + t.Error("Should not be able to update exchange via modify") + } + if od.ID != "1" { + t.Error("Failed to update") + } + if od.ClientID != "1" { + t.Error("Failed to update") + } + if od.WalletAddress != "1" { + t.Error("Failed to update") + } + if od.Type != "1" { + t.Error("Failed to update") + } + if od.Side != "1" { + t.Error("Failed to update") + } + if od.Status != "1" { + t.Error("Failed to update") + } + if od.AssetType != "1" { + t.Error("Failed to update") + } + if od.LastUpdated != updated { + t.Error("Failed to update") + } + if od.Pair.String() != "BTCUSD" { + t.Error("Failed to update") + } + if od.Trades != nil { + t.Error("Failed to update") + } + + om.Trades = append(om.Trades, TradeHistory{TID: "1"}, TradeHistory{TID: "2"}) + od.UpdateOrderFromModify(&om) + if len(od.Trades) != 2 { + t.Error("Failed to add trades") + } + om.Trades[0].Exchange = leet + om.Trades[0].Price = 1337 + om.Trades[0].Fee = 1337 + om.Trades[0].IsMaker = true + om.Trades[0].Timestamp = updated + om.Trades[0].Description = leet + om.Trades[0].Side = UnknownSide + om.Trades[0].Type = UnknownType + om.Trades[0].Amount = 1337 + od.UpdateOrderFromModify(&om) + if od.Trades[0].Exchange == leet { + t.Error("Should not be able to update exchange from update") + } + if od.Trades[0].Price != 1337 { + t.Error("Failed to update trades") + } + if od.Trades[0].Fee != 1337 { + t.Error("Failed to update trades") + } + if !od.Trades[0].IsMaker { + t.Error("Failed to update trades") + } + if od.Trades[0].Timestamp != updated { + t.Error("Failed to update trades") + } + if od.Trades[0].Description != leet { + t.Error("Failed to update trades") + } + if od.Trades[0].Side != UnknownSide { + t.Error("Failed to update trades") + } + if od.Trades[0].Type != UnknownType { + t.Error("Failed to update trades") + } + if od.Trades[0].Amount != 1337 { + t.Error("Failed to update trades") + } +} + +func TestUpdateOrderFromDetail(t *testing.T) { + var leet = "1337" + od := Detail{ + ImmediateOrCancel: false, + HiddenOrder: false, + FillOrKill: false, + PostOnly: false, + Leverage: "", + Price: 0, + Amount: 0, + LimitPriceUpper: 0, + LimitPriceLower: 0, + TriggerPrice: 0, + TargetAmount: 0, + ExecutedAmount: 0, + RemainingAmount: 0, + Fee: 0, + Exchange: "", + ID: "1", + AccountID: "", + ClientID: "", + WalletAddress: "", + Type: "", + Side: "", + Status: "", + AssetType: "", + Date: time.Time{}, + LastUpdated: time.Time{}, + Pair: currency.Pair{}, + Trades: nil, + } + updated := time.Now() + om := Detail{ + ImmediateOrCancel: true, + HiddenOrder: true, + FillOrKill: true, + PostOnly: true, + Leverage: "1", + Price: 1, + Amount: 1, + LimitPriceUpper: 1, + LimitPriceLower: 1, + TriggerPrice: 1, + TargetAmount: 1, + ExecutedAmount: 1, + RemainingAmount: 1, + Fee: 1, + Exchange: "1", + InternalOrderID: "1", + ID: "1", + AccountID: "1", + ClientID: "1", + WalletAddress: "1", + Type: "1", + Side: "1", + Status: "1", + AssetType: "1", + LastUpdated: updated, + Pair: currency.NewPairFromString("BTCUSD"), + Trades: []TradeHistory{}, + } + + od.UpdateOrderFromDetail(&om) + if od.InternalOrderID == "1" { + t.Error("Should not be able to update the internal order ID") + } + if !od.ImmediateOrCancel { + t.Error("Failed to update") + } + if !od.HiddenOrder { + t.Error("Failed to update") + } + if !od.FillOrKill { + t.Error("Failed to update") + } + if !od.PostOnly { + t.Error("Failed to update") + } + if od.Leverage != "1" { + t.Error("Failed to update") + } + if od.Price != 1 { + t.Error("Failed to update") + } + if od.Amount != 1 { + t.Error("Failed to update") + } + if od.LimitPriceLower != 1 { + t.Error("Failed to update") + } + if od.LimitPriceUpper != 1 { + t.Error("Failed to update") + } + if od.TriggerPrice != 1 { + t.Error("Failed to update") + } + if od.TargetAmount != 1 { + t.Error("Failed to update") + } + if od.ExecutedAmount != 1 { + t.Error("Failed to update") + } + if od.RemainingAmount != 1 { + t.Error("Failed to update") + } + if od.Fee != 1 { + t.Error("Failed to update") + } + if od.Exchange != "" { + t.Error("Should not be able to update exchange via modify") + } + if od.ID != "1" { + t.Error("Failed to update") + } + if od.ClientID != "1" { + t.Error("Failed to update") + } + if od.WalletAddress != "1" { + t.Error("Failed to update") + } + if od.Type != "1" { + t.Error("Failed to update") + } + if od.Side != "1" { + t.Error("Failed to update") + } + if od.Status != "1" { + t.Error("Failed to update") + } + if od.AssetType != "1" { + t.Error("Failed to update") + } + if od.LastUpdated != updated { + t.Error("Failed to update") + } + if od.Pair.String() != "BTCUSD" { + t.Error("Failed to update") + } + if od.Trades != nil { + t.Error("Failed to update") + } + + om.Trades = append(om.Trades, TradeHistory{TID: "1"}, TradeHistory{TID: "2"}) + od.UpdateOrderFromDetail(&om) + if len(od.Trades) != 2 { + t.Error("Failed to add trades") + } + om.Trades[0].Exchange = leet + om.Trades[0].Price = 1337 + om.Trades[0].Fee = 1337 + om.Trades[0].IsMaker = true + om.Trades[0].Timestamp = updated + om.Trades[0].Description = leet + om.Trades[0].Side = UnknownSide + om.Trades[0].Type = UnknownType + om.Trades[0].Amount = 1337 + od.UpdateOrderFromDetail(&om) + if od.Trades[0].Exchange == leet { + t.Error("Should not be able to update exchange from update") + } + if od.Trades[0].Price != 1337 { + t.Error("Failed to update trades") + } + if od.Trades[0].Fee != 1337 { + t.Error("Failed to update trades") + } + if !od.Trades[0].IsMaker { + t.Error("Failed to update trades") + } + if od.Trades[0].Timestamp != updated { + t.Error("Failed to update trades") + } + if od.Trades[0].Description != leet { + t.Error("Failed to update trades") + } + if od.Trades[0].Side != UnknownSide { + t.Error("Failed to update trades") + } + if od.Trades[0].Type != UnknownType { + t.Error("Failed to update trades") + } + if od.Trades[0].Amount != 1337 { + t.Error("Failed to update trades") + } +} diff --git a/exchanges/order/order_types.go b/exchanges/order/order_types.go index 31c44ff0..5ae4b276 100644 --- a/exchanges/order/order_types.go +++ b/exchanges/order/order_types.go @@ -2,30 +2,14 @@ package order import ( "errors" + "fmt" "time" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" ) -const ( - limitOrder = iota - marketOrder -) - -// Orders variable holds an array of pointers to order structs -var Orders []*Order - -// Order struct holds order values -type Order struct { - OrderID int - Exchange string - Type int - Amount float64 - Price float64 -} - -// vars related to orders +// var error definitions var ( ErrSubmissionIsNil = errors.New("order submission is nil") ErrPairIsEmpty = errors.New("order pair is empty") @@ -35,16 +19,39 @@ var ( ErrPriceMustBeSetIfLimitOrder = errors.New("order price must be set if limit order type is desired") ) -// Submit contains the order submission data +// Submit contains all properties of an order that may be required +// for an order to be created on an exchange +// Each exchange has their own requirements, so not all fields +// are required to be populated type Submit struct { - Pair currency.Pair - OrderType Type - OrderSide Side - TriggerPrice float64 - TargetAmount float64 - Price float64 - Amount float64 - ClientID string + ImmediateOrCancel bool + HiddenOrder bool + FillOrKill bool + PostOnly bool + Leverage string + Price float64 + Amount float64 + LimitPriceUpper float64 + LimitPriceLower float64 + TriggerPrice float64 + TargetAmount float64 + ExecutedAmount float64 + RemainingAmount float64 + Fee float64 + Exchange string + InternalOrderID string + ID string + AccountID string + ClientID string + WalletAddress string + Type Type + Side Side + Status Status + AssetType asset.Item + Date time.Time + LastUpdated time.Time + Pair currency.Pair + Trades []TradeHistory } // SubmitResponse is what is returned after submitting an order to an exchange @@ -54,20 +61,39 @@ type SubmitResponse struct { OrderID string } -// Modify is an order modifyer +// Modify contains all properties of an order +// that may be updated after it has been created +// Each exchange has their own requirements, so not all fields +// are required to be populated type Modify struct { - OrderID string - Type - Side - Price float64 - Amount float64 - LimitPriceUpper float64 - LimitPriceLower float64 - CurrencyPair currency.Pair ImmediateOrCancel bool HiddenOrder bool FillOrKill bool PostOnly bool + Leverage string + Price float64 + Amount float64 + LimitPriceUpper float64 + LimitPriceLower float64 + TriggerPrice float64 + TargetAmount float64 + ExecutedAmount float64 + RemainingAmount float64 + Fee float64 + Exchange string + InternalOrderID string + ID string + AccountID string + ClientID string + WalletAddress string + Type Type + Side Side + Status Status + AssetType asset.Item + Date time.Time + LastUpdated time.Time + Pair currency.Pair + Trades []TradeHistory } // ModifyResponse is an order modifying return type @@ -75,12 +101,113 @@ type ModifyResponse struct { OrderID string } -// CancelAllResponse returns the status from attempting to cancel all orders on -// an exchagne +// Detail contains all properties of an order +// Each exchange has their own requirements, so not all fields +// are required to be populated +type Detail struct { + ImmediateOrCancel bool + HiddenOrder bool + FillOrKill bool + PostOnly bool + Leverage string + Price float64 + Amount float64 + LimitPriceUpper float64 + LimitPriceLower float64 + TriggerPrice float64 + TargetAmount float64 + ExecutedAmount float64 + RemainingAmount float64 + Fee float64 + Exchange string + InternalOrderID string + ID string + AccountID string + ClientID string + WalletAddress string + Type Type + Side Side + Status Status + AssetType asset.Item + Date time.Time + LastUpdated time.Time + Pair currency.Pair + Trades []TradeHistory +} + +// Cancel contains all properties that may be required +// to cancel an order on an exchange +// Each exchange has their own requirements, so not all fields +// are required to be populated +type Cancel struct { + Price float64 + Amount float64 + Exchange string + ID string + AccountID string + ClientID string + WalletAddress string + Type Type + Side Side + Status Status + AssetType asset.Item + Date time.Time + Pair currency.Pair + Trades []TradeHistory +} + +// CancelAllResponse returns the status from attempting to +// cancel all orders on an exchange type CancelAllResponse struct { Status map[string]string } +// TradeHistory holds exchange history data +type TradeHistory struct { + Price float64 + Amount float64 + Fee float64 + Exchange string + TID string + Description string + Type Type + Side Side + Timestamp time.Time + IsMaker bool +} + +// GetOrdersRequest used for GetOrderHistory and GetOpenOrders wrapper functions +type GetOrdersRequest struct { + Type Type + Side Side + StartTicks time.Time + EndTicks time.Time + // Currencies Empty array = all currencies. Some endpoints only support + // singular currency enquiries + Pairs []currency.Pair +} + +// Status defines order status types +type Status string + +// All order status types +const ( + AnyStatus Status = "ANY" + New Status = "NEW" + Active Status = "ACTIVE" + PartiallyCancelled Status = "PARTIALLY_CANCELLED" + PartiallyFilled Status = "PARTIALLY_FILLED" + Filled Status = "FILLED" + Cancelled Status = "CANCELLED" + PendingCancel Status = "PENDING_CANCEL" + InsufficientBalance Status = "INSUFFICIENT_BALANCE" + MarketUnavailable Status = "MARKET_UNAVAILABLE" + Rejected Status = "REJECTED" + Expired Status = "EXPIRED" + Hidden Status = "HIDDEN" + UnknownStatus Status = "UNKNOWN" +) + // Type enforces a standard for order types across the code base type Type string @@ -91,8 +218,8 @@ const ( Market Type = "MARKET" ImmediateOrCancel Type = "IMMEDIATE_OR_CANCEL" Stop Type = "STOP" - TrailingStop Type = "TRAILINGSTOP" - Unknown Type = "UNKNOWN" + TrailingStop Type = "TRAILING_STOP" + UnknownType Type = "UNKNOWN" ) // Side enforces a standard for order sides across the code base @@ -105,78 +232,7 @@ const ( Sell Side = "SELL" Bid Side = "BID" Ask Side = "ASK" - SideUnknown Side = "SIDEUNKNOWN" -) - -// Detail holds order detail data -type Detail struct { - Exchange string - AccountID string - ID string - CurrencyPair currency.Pair - OrderSide Side - OrderType Type - OrderDate time.Time - Status Status - Price float64 - Amount float64 - ExecutedAmount float64 - RemainingAmount float64 - Fee float64 - Trades []TradeHistory -} - -// TradeHistory holds exchange history data -type TradeHistory struct { - Timestamp time.Time - TID string - Price float64 - Amount float64 - Exchange string - Type Type - Side Side - Fee float64 - Description string -} - -// Cancel type required when requesting to cancel an order -type Cancel struct { - AccountID string - OrderID string - CurrencyPair currency.Pair - AssetType asset.Item - WalletAddress string - Side Side -} - -// GetOrdersRequest used for GetOrderHistory and GetOpenOrders wrapper functions -type GetOrdersRequest struct { - OrderType Type - OrderSide Side - StartTicks time.Time - EndTicks time.Time - // Currencies Empty array = all currencies. Some endpoints only support - // singular currency enquiries - Currencies []currency.Pair -} - -// Status defines order status types -type Status string - -// All order status types -const ( - AnyStatus Status = "ANY" - New Status = "NEW" - Active Status = "ACTIVE" - PartiallyCancelled Status = "PARTIALLY_CANCELLED" - PartiallyFilled Status = "PARTIALLY_FILLED" - Filled Status = "FILLED" - Cancelled Status = "CANCELLED" - PendingCancel Status = "PENDING_CANCEL" - Rejected Status = "REJECTED" - Expired Status = "EXPIRED" - Hidden Status = "HIDDEN" - UnknownStatus Status = "UNKNOWN" + UnknownSide Side = "UNKNOWN" ) // ByPrice used for sorting orders by price @@ -193,3 +249,23 @@ type ByDate []Detail // ByOrderSide used for sorting orders by order side (buy sell) type ByOrderSide []Detail + +// ClassificationError returned when an order status +// side or type cannot be recognised +type ClassificationError struct { + Exchange string + OrderID string + Err error +} + +func (o *ClassificationError) Error() string { + if o.OrderID != "" { + return fmt.Sprintf("%s - OrderID: %s classification error: %v", + o.Exchange, + o.OrderID, + o.Err) + } + return fmt.Sprintf("%s - classification error: %v", + o.Exchange, + o.Err) +} diff --git a/exchanges/order/orders.go b/exchanges/order/orders.go index 1fb1d2c7..3d368e95 100644 --- a/exchanges/order/orders.go +++ b/exchanges/order/orders.go @@ -1,7 +1,7 @@ package order import ( - "fmt" + "errors" "sort" "strings" "time" @@ -9,57 +9,6 @@ import ( "github.com/thrasher-corp/gocryptotrader/currency" ) -// NewOrder creates a new order and returns a an orderID -func NewOrder(exchangeName string, amount, price float64) int { - ord := &Order{} - if len(Orders) == 0 { - ord.OrderID = 0 - } else { - ord.OrderID = len(Orders) - } - - ord.Exchange = exchangeName - ord.Amount = amount - ord.Price = price - Orders = append(Orders, ord) - return ord.OrderID -} - -// DeleteOrder deletes orders by ID and returns state -func DeleteOrder(orderID int) bool { - for i := range Orders { - if Orders[i].OrderID == orderID { - Orders = append(Orders[:i], Orders[i+1:]...) - return true - } - } - return false -} - -// GetOrdersByExchange returns order pointer grouped by exchange -func GetOrdersByExchange(exchange string) []*Order { - var orders []*Order - for i := range Orders { - if Orders[i].Exchange == exchange { - orders = append(orders, Orders[i]) - } - } - if len(orders) > 0 { - return orders - } - return nil -} - -// GetOrderByOrderID returns order pointer by ID -func GetOrderByOrderID(orderID int) *Order { - for i := range Orders { - if Orders[i].OrderID == orderID { - return Orders[i] - } - } - return nil -} - // Validate checks the supplied data and returns whether or not it's valid func (s *Submit) Validate() error { if s == nil { @@ -70,14 +19,14 @@ func (s *Submit) Validate() error { return ErrPairIsEmpty } - if s.OrderSide != Buy && - s.OrderSide != Sell && - s.OrderSide != Bid && - s.OrderSide != Ask { + if s.Side != Buy && + s.Side != Sell && + s.Side != Bid && + s.Side != Ask { return ErrSideIsInvalid } - if s.OrderType != Market && s.OrderType != Limit { + if s.Type != Market && s.Type != Limit { return ErrTypeIsInvalid } @@ -85,13 +34,311 @@ func (s *Submit) Validate() error { return ErrAmountIsInvalid } - if s.OrderType == Limit && s.Price <= 0 { + if s.Type == Limit && s.Price <= 0 { return ErrPriceMustBeSetIfLimitOrder } return nil } +// UpdateOrderFromDetail Will update an order detail (used in order management) +// by comparing passed in and existing values +func (d *Detail) UpdateOrderFromDetail(m *Detail) { + var updated bool + if d.ImmediateOrCancel != m.ImmediateOrCancel { + d.ImmediateOrCancel = m.ImmediateOrCancel + updated = true + } + if d.HiddenOrder != m.HiddenOrder { + d.HiddenOrder = m.HiddenOrder + updated = true + } + if d.FillOrKill != m.FillOrKill { + d.FillOrKill = m.FillOrKill + updated = true + } + if m.Price > 0 && m.Price != d.Price { + d.Price = m.Price + updated = true + } + if m.Amount > 0 && m.Amount != d.Amount { + d.Amount = m.Amount + updated = true + } + if m.LimitPriceUpper > 0 && m.LimitPriceUpper != d.LimitPriceUpper { + d.LimitPriceUpper = m.LimitPriceUpper + updated = true + } + if m.LimitPriceLower > 0 && m.LimitPriceLower != d.LimitPriceLower { + d.LimitPriceLower = m.LimitPriceLower + updated = true + } + if m.TriggerPrice > 0 && m.TriggerPrice != d.TriggerPrice { + d.TriggerPrice = m.TriggerPrice + updated = true + } + if m.TargetAmount > 0 && m.TargetAmount != d.TargetAmount { + d.TargetAmount = m.TargetAmount + updated = true + } + if m.ExecutedAmount > 0 && m.ExecutedAmount != d.ExecutedAmount { + d.ExecutedAmount = m.ExecutedAmount + updated = true + } + if m.Fee > 0 && m.Fee != d.Fee { + d.Fee = m.Fee + updated = true + } + if m.AccountID != "" && m.AccountID != d.AccountID { + d.AccountID = m.AccountID + updated = true + } + if m.PostOnly != d.PostOnly { + d.PostOnly = m.PostOnly + updated = true + } + if !m.Pair.IsEmpty() && m.Pair != d.Pair { + d.Pair = m.Pair + updated = true + } + if m.Leverage != "" && m.Leverage != d.Leverage { + d.Leverage = m.Leverage + updated = true + } + if m.ClientID != "" && m.ClientID != d.ClientID { + d.ClientID = m.ClientID + updated = true + } + if m.WalletAddress != "" && m.WalletAddress != d.WalletAddress { + d.WalletAddress = m.WalletAddress + updated = true + } + if m.Type != "" && m.Type != d.Type { + d.Type = m.Type + updated = true + } + if m.Side != "" && m.Side != d.Side { + d.Side = m.Side + updated = true + } + if m.Status != "" && m.Status != d.Status { + d.Status = m.Status + updated = true + } + if m.AssetType != "" && m.AssetType != d.AssetType { + d.AssetType = m.AssetType + updated = true + } + if m.Trades != nil { + for x := range m.Trades { + var found bool + for y := range d.Trades { + if d.Trades[y].TID != m.Trades[x].TID { + continue + } + found = true + if d.Trades[y].Fee != m.Trades[x].Fee { + d.Trades[y].Fee = m.Trades[x].Fee + updated = true + } + if m.Trades[y].Price != 0 && d.Trades[y].Price != m.Trades[x].Price { + d.Trades[y].Price = m.Trades[x].Price + updated = true + } + if d.Trades[y].Side != m.Trades[x].Side { + d.Trades[y].Side = m.Trades[x].Side + updated = true + } + if d.Trades[y].Type != m.Trades[x].Type { + d.Trades[y].Type = m.Trades[x].Type + updated = true + } + if d.Trades[y].Description != m.Trades[x].Description { + d.Trades[y].Description = m.Trades[x].Description + updated = true + } + if m.Trades[y].Amount != 0 && d.Trades[y].Amount != m.Trades[x].Amount { + d.Trades[y].Amount = m.Trades[x].Amount + updated = true + } + if d.Trades[y].Timestamp != m.Trades[x].Timestamp { + d.Trades[y].Timestamp = m.Trades[x].Timestamp + updated = true + } + if d.Trades[y].IsMaker != m.Trades[x].IsMaker { + d.Trades[y].IsMaker = m.Trades[x].IsMaker + updated = true + } + } + if !found { + d.Trades = append(d.Trades, m.Trades[x]) + updated = true + } + m.RemainingAmount -= m.Trades[x].Amount + } + } + if m.RemainingAmount > 0 && m.RemainingAmount != d.RemainingAmount { + d.RemainingAmount = m.RemainingAmount + updated = true + } + if updated { + if d.LastUpdated == m.LastUpdated { + d.LastUpdated = time.Now() + } else { + d.LastUpdated = m.LastUpdated + } + } +} + +// UpdateOrderFromModify Will update an order detail (used in order management) +// by comparing passed in and existing values +func (d *Detail) UpdateOrderFromModify(m *Modify) { + var updated bool + if d.ImmediateOrCancel != m.ImmediateOrCancel { + d.ImmediateOrCancel = m.ImmediateOrCancel + updated = true + } + if d.HiddenOrder != m.HiddenOrder { + d.HiddenOrder = m.HiddenOrder + updated = true + } + if d.FillOrKill != m.FillOrKill { + d.FillOrKill = m.FillOrKill + updated = true + } + if m.Price > 0 && m.Price != d.Price { + d.Price = m.Price + updated = true + } + if m.Amount > 0 && m.Amount != d.Amount { + d.Amount = m.Amount + updated = true + } + if m.LimitPriceUpper > 0 && m.LimitPriceUpper != d.LimitPriceUpper { + d.LimitPriceUpper = m.LimitPriceUpper + updated = true + } + if m.LimitPriceLower > 0 && m.LimitPriceLower != d.LimitPriceLower { + d.LimitPriceLower = m.LimitPriceLower + updated = true + } + if m.TriggerPrice > 0 && m.TriggerPrice != d.TriggerPrice { + d.TriggerPrice = m.TriggerPrice + updated = true + } + if m.TargetAmount > 0 && m.TargetAmount != d.TargetAmount { + d.TargetAmount = m.TargetAmount + updated = true + } + if m.ExecutedAmount > 0 && m.ExecutedAmount != d.ExecutedAmount { + d.ExecutedAmount = m.ExecutedAmount + updated = true + } + if m.Fee > 0 && m.Fee != d.Fee { + d.Fee = m.Fee + updated = true + } + if m.AccountID != "" && m.AccountID != d.AccountID { + d.AccountID = m.AccountID + updated = true + } + if m.PostOnly != d.PostOnly { + d.PostOnly = m.PostOnly + updated = true + } + if !m.Pair.IsEmpty() && m.Pair != d.Pair { + d.Pair = m.Pair + updated = true + } + if m.Leverage != "" && m.Leverage != d.Leverage { + d.Leverage = m.Leverage + updated = true + } + if m.ClientID != "" && m.ClientID != d.ClientID { + d.ClientID = m.ClientID + updated = true + } + if m.WalletAddress != "" && m.WalletAddress != d.WalletAddress { + d.WalletAddress = m.WalletAddress + updated = true + } + if m.Type != "" && m.Type != d.Type { + d.Type = m.Type + updated = true + } + if m.Side != "" && m.Side != d.Side { + d.Side = m.Side + updated = true + } + if m.Status != "" && m.Status != d.Status { + d.Status = m.Status + updated = true + } + if m.AssetType != "" && m.AssetType != d.AssetType { + d.AssetType = m.AssetType + updated = true + } + if m.Trades != nil { + for x := range m.Trades { + var found bool + for y := range d.Trades { + if d.Trades[y].TID != m.Trades[x].TID { + continue + } + found = true + if d.Trades[y].Fee != m.Trades[x].Fee { + d.Trades[y].Fee = m.Trades[x].Fee + updated = true + } + if m.Trades[y].Price != 0 && d.Trades[y].Price != m.Trades[x].Price { + d.Trades[y].Price = m.Trades[x].Price + updated = true + } + if d.Trades[y].Side != m.Trades[x].Side { + d.Trades[y].Side = m.Trades[x].Side + updated = true + } + if d.Trades[y].Type != m.Trades[x].Type { + d.Trades[y].Type = m.Trades[x].Type + updated = true + } + if d.Trades[y].Description != m.Trades[x].Description { + d.Trades[y].Description = m.Trades[x].Description + updated = true + } + if m.Trades[y].Amount != 0 && d.Trades[y].Amount != m.Trades[x].Amount { + d.Trades[y].Amount = m.Trades[x].Amount + updated = true + } + if d.Trades[y].Timestamp != m.Trades[x].Timestamp { + d.Trades[y].Timestamp = m.Trades[x].Timestamp + updated = true + } + if d.Trades[y].IsMaker != m.Trades[x].IsMaker { + d.Trades[y].IsMaker = m.Trades[x].IsMaker + updated = true + } + } + if !found { + d.Trades = append(d.Trades, m.Trades[x]) + updated = true + } + m.RemainingAmount -= m.Trades[x].Amount + } + } + if m.RemainingAmount > 0 && m.RemainingAmount != d.RemainingAmount { + d.RemainingAmount = m.RemainingAmount + updated = true + } + if updated { + if d.LastUpdated == m.LastUpdated { + d.LastUpdated = time.Now() + } else { + d.LastUpdated = m.LastUpdated + } + } +} + // String implements the stringer interface func (t Type) String() string { return string(t) @@ -126,7 +373,7 @@ func FilterOrdersBySide(orders *[]Detail, side Side) { var filteredOrders []Detail for i := range *orders { - if strings.EqualFold(string((*orders)[i].OrderSide), string(side)) { + if strings.EqualFold(string((*orders)[i].Side), string(side)) { filteredOrders = append(filteredOrders, (*orders)[i]) } } @@ -143,7 +390,7 @@ func FilterOrdersByType(orders *[]Detail, orderType Type) { var filteredOrders []Detail for i := range *orders { - if strings.EqualFold(string((*orders)[i].OrderType), string(orderType)) { + if strings.EqualFold(string((*orders)[i].Type), string(orderType)) { filteredOrders = append(filteredOrders, (*orders)[i]) } } @@ -163,8 +410,8 @@ func FilterOrdersByTickRange(orders *[]Detail, startTicks, endTicks time.Time) { var filteredOrders []Detail for i := range *orders { - if (*orders)[i].OrderDate.Unix() >= startTicks.Unix() && - (*orders)[i].OrderDate.Unix() <= endTicks.Unix() { + if (*orders)[i].Date.Unix() >= startTicks.Unix() && + (*orders)[i].Date.Unix() <= endTicks.Unix() { filteredOrders = append(filteredOrders, (*orders)[i]) } } @@ -184,7 +431,7 @@ func FilterOrdersByCurrencies(orders *[]Detail, currencies []currency.Pair) { for i := range *orders { matchFound := false for _, c := range currencies { - if !matchFound && (*orders)[i].CurrencyPair.EqualIncludeReciprocal(c) { + if !matchFound && (*orders)[i].Pair.EqualIncludeReciprocal(c) { matchFound = true } } @@ -223,7 +470,7 @@ func (b ByOrderType) Len() int { } func (b ByOrderType) Less(i, j int) bool { - return b[i].OrderType.String() < b[j].OrderType.String() + return b[i].Type.String() < b[j].Type.String() } func (b ByOrderType) Swap(i, j int) { @@ -244,7 +491,7 @@ func (b ByCurrency) Len() int { } func (b ByCurrency) Less(i, j int) bool { - return b[i].CurrencyPair.String() < b[j].CurrencyPair.String() + return b[i].Pair.String() < b[j].Pair.String() } func (b ByCurrency) Swap(i, j int) { @@ -265,7 +512,7 @@ func (b ByDate) Len() int { } func (b ByDate) Less(i, j int) bool { - return b[i].OrderDate.Unix() < b[j].OrderDate.Unix() + return b[i].Date.Unix() < b[j].Date.Unix() } func (b ByDate) Swap(i, j int) { @@ -286,7 +533,7 @@ func (b ByOrderSide) Len() int { } func (b ByOrderSide) Less(i, j int) bool { - return b[i].OrderSide.String() < b[j].OrderSide.String() + return b[i].Side.String() < b[j].Side.String() } func (b ByOrderSide) Swap(i, j int) { @@ -317,7 +564,7 @@ func StringToOrderSide(side string) (Side, error) { case strings.EqualFold(side, AnySide.String()): return AnySide, nil default: - return Side(""), fmt.Errorf("%s not recognised as side type", side) + return UnknownSide, errors.New(side + " not recognised as order side") } } @@ -329,16 +576,20 @@ func StringToOrderType(oType string) (Type, error) { return Limit, nil case strings.EqualFold(oType, Market.String()): return Market, nil - case strings.EqualFold(oType, ImmediateOrCancel.String()): + case strings.EqualFold(oType, ImmediateOrCancel.String()), + strings.EqualFold(oType, "immediate or cancel"): return ImmediateOrCancel, nil - case strings.EqualFold(oType, Stop.String()): + case strings.EqualFold(oType, Stop.String()), + strings.EqualFold(oType, "stop loss"), + strings.EqualFold(oType, "stop_loss"): return Stop, nil - case strings.EqualFold(oType, TrailingStop.String()): + case strings.EqualFold(oType, TrailingStop.String()), + strings.EqualFold(oType, "trailing stop"): return TrailingStop, nil case strings.EqualFold(oType, AnyType.String()): return AnyType, nil default: - return Unknown, fmt.Errorf("%s not recognised as order type", oType) + return UnknownType, errors.New(oType + " not recognised as order type") } } @@ -348,17 +599,27 @@ func StringToOrderStatus(status string) (Status, error) { switch { case strings.EqualFold(status, AnyStatus.String()): return AnyStatus, nil - case strings.EqualFold(status, New.String()): + case strings.EqualFold(status, New.String()), + strings.EqualFold(status, "placed"): return New, nil case strings.EqualFold(status, Active.String()): return Active, nil - case strings.EqualFold(status, PartiallyFilled.String()): + case strings.EqualFold(status, PartiallyFilled.String()), + strings.EqualFold(status, "partially matched"), + strings.EqualFold(status, "partially filled"): return PartiallyFilled, nil - case strings.EqualFold(status, Filled.String()): + case strings.EqualFold(status, Filled.String()), + strings.EqualFold(status, "fully matched"), + strings.EqualFold(status, "fully filled"): return Filled, nil + case strings.EqualFold(status, PartiallyCancelled.String()), + strings.EqualFold(status, "partially cancelled"): + return PartiallyCancelled, nil case strings.EqualFold(status, Cancelled.String()): return Cancelled, nil - case strings.EqualFold(status, PendingCancel.String()): + case strings.EqualFold(status, PendingCancel.String()), + strings.EqualFold(status, "pending cancel"), + strings.EqualFold(status, "pending cancellation"): return PendingCancel, nil case strings.EqualFold(status, Rejected.String()): return Rejected, nil @@ -366,7 +627,11 @@ func StringToOrderStatus(status string) (Status, error) { return Expired, nil case strings.EqualFold(status, Hidden.String()): return Hidden, nil + case strings.EqualFold(status, InsufficientBalance.String()): + return InsufficientBalance, nil + case strings.EqualFold(status, MarketUnavailable.String()): + return MarketUnavailable, nil default: - return UnknownStatus, fmt.Errorf("%s not recognised as order STATUS", status) + return UnknownStatus, errors.New(status + " not recognised as order status") } } diff --git a/exchanges/order/orders_test.go b/exchanges/order/orders_test.go deleted file mode 100644 index e880e03f..00000000 --- a/exchanges/order/orders_test.go +++ /dev/null @@ -1,37 +0,0 @@ -package order - -import ( - "testing" -) - -func TestNewOrder(t *testing.T) { - ID := NewOrder("OKEX", 2000, 20.00) - if ID != 0 { - t.Error("Orders_test.go NewOrder() - Error") - } - ID = NewOrder("BATMAN", 400, 25.00) - if ID != 1 { - t.Error("Orders_test.go NewOrder() - Error") - } -} - -func TestDeleteOrder(t *testing.T) { - if value := DeleteOrder(0); !value { - t.Error("Orders_test.go DeleteOrder() - Error") - } - if value := DeleteOrder(100); value { - t.Error("Orders_test.go DeleteOrder() - Error") - } -} - -func TestGetOrdersByExchange(t *testing.T) { - if value := GetOrdersByExchange("OKEX"); len(value) != 0 { - t.Error("Orders_test.go GetOrdersByExchange() - Error") - } -} - -func TestGetOrderByOrderID(t *testing.T) { - if value := GetOrderByOrderID(69); value != nil { - t.Error("Orders_test.go GetOrdersByExchange() - Error") - } -} diff --git a/exchanges/poloniex/poloniex.go b/exchanges/poloniex/poloniex.go index 12ce1fb9..3ebbc493 100644 --- a/exchanges/poloniex/poloniex.go +++ b/exchanges/poloniex/poloniex.go @@ -48,8 +48,6 @@ const ( poloniexActiveLoans = "returnActiveLoans" poloniexLendingHistory = "returnLendingHistory" poloniexAutoRenew = "toggleAutoRenew" - - poloniexDateLayout = "2006-01-02 15:04:05" ) // Poloniex is the overarching type across the poloniex package diff --git a/exchanges/poloniex/poloniex_live_test.go b/exchanges/poloniex/poloniex_live_test.go index f2ad34f2..36c5afee 100644 --- a/exchanges/poloniex/poloniex_live_test.go +++ b/exchanges/poloniex/poloniex_live_test.go @@ -34,5 +34,7 @@ func TestMain(m *testing.M) { log.Fatal("Poloniex setup error", err) } log.Printf(sharedtestvalues.LiveTesting, p.Name, p.API.Endpoints.URL) + p.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride() + p.Websocket.TrafficAlert = sharedtestvalues.GetWebsocketStructChannelOverride() os.Exit(m.Run()) } diff --git a/exchanges/poloniex/poloniex_mock_test.go b/exchanges/poloniex/poloniex_mock_test.go index 546ee7d3..0df790ae 100644 --- a/exchanges/poloniex/poloniex_mock_test.go +++ b/exchanges/poloniex/poloniex_mock_test.go @@ -45,7 +45,8 @@ func TestMain(m *testing.M) { p.HTTPClient = newClient p.API.Endpoints.URL = serverDetails - + p.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride() + p.Websocket.TrafficAlert = sharedtestvalues.GetWebsocketStructChannelOverride() log.Printf(sharedtestvalues.MockTesting, p.Name, p.API.Endpoints.URL) os.Exit(m.Run()) } diff --git a/exchanges/poloniex/poloniex_test.go b/exchanges/poloniex/poloniex_test.go index 0af5ae6d..9b793f9e 100644 --- a/exchanges/poloniex/poloniex_test.go +++ b/exchanges/poloniex/poloniex_test.go @@ -1,7 +1,6 @@ package poloniex import ( - "encoding/json" "net/http" "testing" "time" @@ -215,7 +214,7 @@ func TestFormatWithdrawPermissions(t *testing.T) { func TestGetActiveOrders(t *testing.T) { t.Parallel() var getOrdersRequest = order.GetOrdersRequest{ - OrderType: order.AnyType, + Type: order.AnyType, } _, err := p.GetActiveOrders(&getOrdersRequest) @@ -232,7 +231,7 @@ func TestGetActiveOrders(t *testing.T) { func TestGetOrderHistory(t *testing.T) { t.Parallel() var getOrdersRequest = order.GetOrdersRequest{ - OrderType: order.AnyType, + Type: order.AnyType, } _, err := p.GetOrderHistory(&getOrdersRequest) @@ -261,11 +260,11 @@ func TestSubmitOrder(t *testing.T) { Base: currency.BTC, Quote: currency.LTC, }, - OrderSide: order.Buy, - OrderType: order.Market, - Price: 10, - Amount: 10000000, - ClientID: "hi", + Side: order.Buy, + Type: order.Market, + Price: 10, + Amount: 10000000, + ClientID: "hi", } response, err := p.SubmitOrder(orderSubmission) @@ -285,10 +284,10 @@ func TestCancelExchangeOrder(t *testing.T) { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } var orderCancellation = &order.Cancel{ - OrderID: "1", + ID: "1", WalletAddress: core.BitcoinDonationAddress, AccountID: "1", - CurrencyPair: currency.NewPair(currency.LTC, currency.BTC), + Pair: currency.NewPair(currency.LTC, currency.BTC), } err := p.CancelOrder(orderCancellation) @@ -310,10 +309,10 @@ func TestCancelAllExchangeOrders(t *testing.T) { currencyPair := currency.NewPair(currency.LTC, currency.BTC) var orderCancellation = &order.Cancel{ - OrderID: "1", + ID: "1", WalletAddress: core.BitcoinDonationAddress, AccountID: "1", - CurrencyPair: currencyPair, + Pair: currencyPair, } resp, err := p.CancelAllOrders(orderCancellation) @@ -336,7 +335,7 @@ func TestModifyOrder(t *testing.T) { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - _, err := p.ModifyOrder(&order.Modify{OrderID: "1337", Price: 1337}) + _, err := p.ModifyOrder(&order.Modify{ID: "1337", Price: 1337}) switch { case areTestAPIKeysSet() && err != nil && mockTests: t.Error("ModifyOrder() error", err) @@ -415,24 +414,6 @@ func TestGetDepositAddress(t *testing.T) { } } -func TestWsHandleAccountData(t *testing.T) { - t.Parallel() - p.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride() - jsons := []string{ - `[["n",225,807230187,0,"1000.00000000","0.10000000","2018-11-07 16:42:42"],["b",267,"e","-0.10000000"]]`, - `[["o",807230187,"0.00000000"],["b",267,"e","0.10000000"]]`, - `[["t", 12345, "0.03000000", "0.50000000", "0.00250000", 0, 6083059, "0.00000375", "2018-09-08 05:54:09"]]`, - } - for i := range jsons { - var result [][]interface{} - err := json.Unmarshal([]byte(jsons[i]), &result) - if err != nil { - t.Error(err) - } - p.wsHandleAccountData(result) - } -} - // TestWsAuth dials websocket, sends login request. // Will receive a message only on failure func TestWsAuth(t *testing.T) { @@ -454,7 +435,7 @@ func TestWsAuth(t *testing.T) { } p.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride() p.Websocket.TrafficAlert = sharedtestvalues.GetWebsocketStructChannelOverride() - go p.WsHandleData() + go p.wsReadData() err = p.wsSendAuthorisedCommand("subscribe") if err != nil { t.Fatal(err) @@ -467,3 +448,85 @@ func TestWsAuth(t *testing.T) { } timer.Stop() } + +func TestWsSubAck(t *testing.T) { + pressXToJSON := []byte(`[1002, 1]`) + err := p.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsTicker(t *testing.T) { + err := p.getCurrencyIDMap() + if err != nil { + t.Error(err) + } + pressXToJSON := []byte(`[1002, null, [ 50, "382.98901522", "381.99755898", "379.41296309", "-0.04312950", "14969820.94951828", "38859.58435407", 0, "412.25844455", "364.56122072" ] ]`) + err = p.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsExchangeVolume(t *testing.T) { + err := p.getCurrencyIDMap() + if err != nil { + t.Error(err) + } + pressXToJSON := []byte(`[1003,null,["2018-11-07 16:26",5804,{"BTC":"3418.409","ETH":"2645.921","USDT":"10832502.689","USDC":"1578020.908"}]]`) + err = p.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsTrades(t *testing.T) { + err := p.getCurrencyIDMap() + if err != nil { + t.Error(err) + } + pressXToJSON := []byte(`[14, 8768, [["t", "42706057", 1, "0.05567134", "0.00181421", 1522877119]]]`) + err = p.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsPriceAggregateOrderbook(t *testing.T) { + err := p.getCurrencyIDMap() + if err != nil { + t.Error(err) + } + pressXToJSON := []byte(`[148,827987828,[["i",{"currencyPair":"BTC_ETH","orderBook":[{"0.02311264":"2.20557811","0.02311600":"84.08160000","0.02312999":"8.64968477","0.02313000":"1.04012298","0.02313832":"8.65139901","0.02314389":"8.17308984","0.02314390":"85.00000000","0.02315000":"0.22107450","0.02316698":"3.58606790","0.02316699":"18.28000000","0.02316700":"3.38902934","0.02317926":"60.56000000","0.02317927":"0.00593568","0.02322065":"586.54000000","0.02322448":"43.65239848","0.02322500":"0.22107450","0.02325301":"0.24886762","0.02326884":"0.86320742","0.02328311":"0.08824715","0.02328536":"0.04303557","0.02328537":"0.31420536","0.02328584":"0.01000000","0.02328626":"0.05000000","0.02328870":"0.30372277","0.02329327":"0.05000000","0.02329517":"0.00590632","0.02329681":"0.05000000","0.02329695":"0.05000000","0.02329828":"0.05000000","0.02329836":"0.75160000","0.02329850":"0.05609507","0.02329867":"0.00870000","0.02330000":"1.87647637","0.02330070":"0.03410191","0.02330381":"0.05000000","0.02331083":"0.16078678","0.02331243":"0.02000000","0.02331556":"0.05770631","0.02331557":"0.05770630","0.02331558":"0.05770630","0.02332181":"0.03040000","0.02332235":"0.21717356","0.02333128":"0.04081941","0.02333191":"0.00996007","0.02333333":"0.76706001","0.02333377":"0.01000000","0.02333753":"0.26851522","0.02334247":"1.00000000","0.02334475":"0.08619536","0.02334476":"0.08619532","0.02334514":"0.08619249","0.02334515":"0.08619246","0.02334563":"0.08618886","0.02334615":"0.08618506","0.02334999":"82.25709323","0.02335000":"2.00920859","0.02335954":"0.59849633","0.02336562":"0.05151537","0.02337443":"3.24458234","0.02337500":"0.22107450","0.02337945":"0.00887856","0.02339740":"0.00570128","0.02339836":"0.75170000","0.02339850":"0.05609507","0.02340000":"8.35534261","0.02340222":"0.50014040","0.02341165":"0.00587696","0.02341488":"0.02229647","0.02342000":"0.03000000","0.02342803":"0.51603525","0.02343000":"1.99700000","0.02344057":"0.20000000","0.02344501":"0.50009878","0.02344864":"0.04468562","0.02345000":"1.22607450","0.02346052":"0.45364582","0.02348918":"0.01138875","0.02349000":"6.01963602","0.02349836":"0.75180000","0.02349850":"0.05609507","0.02350000":"23.38752043","0.02350019":"0.02363135","0.02350369":"0.04379185","0.02350587":"0.55500002","0.02350588":"0.55500001","0.02352500":"0.22107450","0.02352801":"0.01736539","0.02352871":"0.00584760","0.02353636":"0.59849633","0.02354000":"0.08984535","0.02354023":"0.01419758","0.02354800":"0.05000000","0.02355000":"1.00000000","0.02356542":"0.01268000","0.02356600":"0.04133829","0.02357107":"0.03040000","0.02357377":"5.53105357","0.02357945":"2.00000000","0.02358000":"0.05512230","0.02359836":"0.75190000","0.02359850":"0.05609507","0.02359902":"0.30000000","0.02360000":"5.59692724","0.02360795":"0.01313436","0.02361184":"0.74575578","0.02361788":"0.10951236","0.02363000":"1.00000000","0.02363690":"0.02437918","0.02364090":"4.23191724","0.02364200":"0.00563594","0.02364296":"175.56000000","0.02364636":"0.00581874","0.02365000":"1.00500000","0.02365298":"3.02119704","0.02365532":"50.00000000","0.02367945":"1.00000000","0.02369836":"0.75200000","0.02370000":"0.80891942","0.02370060":"0.01000000","0.02370409":"3.95849091","0.02371452":"0.59849633","0.02371605":"0.10000000","0.02372441":"1.00000000","0.02374515":"0.07733034","0.02375000":"11.13820000","0.02375489":"0.10000000","0.02376460":"0.00578988","0.02377976":"0.25726394","0.02379400":"3.49253617","0.02379850":"0.05609507","0.02380000":"25.12680676","0.02381381":"0.28349641","0.02381560":"5.21495064","0.02382270":"0.82483502","0.02382554":"17.00801738","0.02382885":"0.00884413","0.02383610":"5.61081721","0.02383690":"0.02379185","0.02384024":"0.01824364","0.02384265":"3.78056119","0.02386942":"0.01332000","0.02388343":"0.00576100","0.02388489":"0.10000000","0.02389000":"10.00000000","0.02389403":"0.59849633","0.02389775":"0.04000000","0.02390000":"0.33845599","0.02390161":"0.01320844","0.02393000":"5.00000000","0.02395000":"0.00500000","0.02395905":"0.10000000","0.02396145":"0.45364582","0.02396728":"0.25000000","0.02397000":"0.20000000","0.02397945":"1.32526449","0.02398100":"0.07150300","0.02398900":"0.04962855","0.02399000":"0.10000000","0.02399641":"0.75010000","0.02399850":"0.05609507","0.02399890":"0.10000000","0.02399923":"0.03234736","0.02399999":"0.01390173","0.02400000":"78.68354747","0.02400285":"0.00573214","0.02400743":"0.27244440","0.02401560":"1.09209339","0.02402070":"0.02871320","0.02402330":"0.01420000","0.02403549":"1.15760251","0.02404000":"0.00512238","0.02406526":"0.06435309","0.02407490":"0.59849633","0.02407511":"0.22916250","0.02408001":"0.10000000","0.02408900":"0.10000000","0.02409434":"0.55695311","0.02409560":"0.90000000","0.02409641":"0.75020000","0.02410000":"10.00829876","0.02411745":"0.00720165","0.02412287":"0.00570376","0.02412560":"1.00000000","0.02413000":"0.05095410","0.02413424":"0.01313436","0.02415200":"0.00549829","0.02415247":"0.01916181","0.02415900":"9.00000000","0.02417109":"0.01400000","0.02418080":"0.10000000","0.02418900":"0.02391850","0.02419132":"0.99850000","0.02419641":"0.75030000","0.02420000":"4.98736614","0.02420510":"5.00000000","0.02421887":"0.00454382","0.02424000":"0.46055829","0.02424349":"0.00567538","0.02424504":"4.03787835","0.02424849":"0.61837624","0.02425714":"0.59849633","0.02426315":"0.01000000","0.02428900":"0.10000000","0.02429641":"0.75040000","0.02429981":"0.29848383","0.02430000":"0.44132634","0.02430922":"0.11114354","0.02431247":"59.20000000","0.02432377":"0.17599103","0.02434712":"0.75000000","0.02434824":"0.00720165","0.02436376":"6.53422874","0.02436471":"0.00564700","0.02436630":"0.00503621","0.02437654":"0.02373257","0.02438900":"0.02239185","0.02438910":"0.00504710","0.02439641":"0.75050000","0.02440000":"10.00409837","0.02440820":"7.89038111","0.02444076":"0.59849633","0.02446013":"0.22682291","0.02446281":"0.02011990","0.02447201":"50.00000000","0.02447277":"0.01468000","0.02448900":"0.10000000","0.02449641":"0.75060000","0.02449993":"0.03203089","0.02450000":"7.90099804","0.02451172":"0.06941288","0.02452000":"0.00494192","0.02452220":"0.13664099","0.02453205":"6.00000000","0.02454430":"61.92378648","0.02458900":"0.02292401","0.02459641":"0.75070000","0.02460000":"0.15813010","0.02461943":"0.31185184","0.02462577":"0.59849633","0.02463340":"0.00473821","0.02465000":"0.08113000","0.02468900":"0.10000000","0.02469000":"0.10000000","0.02469442":"0.00720165","0.02469641":"0.75080000","0.02470000":"7.71371820","0.02475857":"0.16516852","0.02476952":"1.00000000","0.02477337":"0.00500000","0.02477444":"0.01540000","0.02477504":"0.02111791","0.02477724":"0.06258849","0.02479573":"0.31347716","0.02479641":"0.75090000","0.02480000":"10.00000000","0.02480748":"0.23814629","0.02481218":"0.59849633","0.02482560":"1.00000000","0.02482784":"0.01228842","0.02483000":"0.01500000","0.02486093":"4.00000000","0.02486501":"0.50000000","0.02487660":"0.90000000","0.02488749":"0.00409844","0.02488900":"0.10000000","0.02488977":"0.04000000","0.02489000":"0.35163940","0.02489641":"0.75100000","0.02490000":"10.58814327","0.02491000":"0.00608537","0.02491500":"1.71477436","0.02491966":"0.00409315","0.02493728":"0.25000000","0.02495701":"0.00721241","0.02495881":"0.45364582","0.02496508":"0.45122312","0.02499000":"1.29455628","0.02499036":"0.69792104","0.02499641":"0.75110000","0.02499999":"0.04948379","0.02500000":"119.42337109","0.02500069":"0.11974446","0.02503742":"0.00998500","0.02505233":"8.00000000","0.02506435":"0.45600000","0.02507612":"0.01620000","0.02508727":"0.02215584","0.02508900":"0.10000000","0.02509641":"0.75120000","0.02511088":"1.81386399","0.02514830":"0.01500000","0.02515188":"2.81540025","0.02515200":"0.50014866","0.02515600":"0.00720165","0.02515800":"0.05000000","0.02515987":"0.12949064","0.02517000":"1.18940537","0.02518080":"0.10000000","0.02519641":"0.75130000","0.02520000":"20.80000000","0.02520548":"0.50029134","0.02522000":"1.94462357","0.02524800":"0.05000000","0.02529641":"0.75140000","0.02529925":"0.13992865","0.02530000":"10.11581028","0.02530177":"0.32847591","0.02535134":"2.99550001","0.02535408":"0.01000000","0.02535746":"0.28697654","0.02535752":"5.51788977","0.02536270":"0.01000000","0.02536962":"0.01000000","0.02538011":"0.01700000","0.02538758":"0.07045847","0.02539136":"0.49999222","0.02539641":"0.75150000","0.02539761":"0.02327362","0.02540000":"21.00393701","0.02540549":"0.12890839","0.02543942":"0.25000000","0.02544346":"0.06078266","0.02545749":"0.45364582","0.02547368":"0.01000000","0.02548000":"0.00493328","0.02549641":"0.75160000","0.02550000":"44.09979658","0.02552638":"0.30000000","0.02553003":"0.16000000","0.02554581":"0.20000000","0.02555080":"0.31508157","0.02555555":"0.22235762","0.02557201":"50.00000000","0.02559641":"0.75170000","0.02559999":"0.01956367","0.02560000":"1.05956590","0.02560417":"6.91427454","0.02566000":"0.12766398","0.02566176":"0.76304697","0.02568001":"20.00000000","0.02568179":"0.01784000","0.02569100":"3.03013591","0.02569641":"0.75180000","0.02569756":"0.21625244","0.02570000":"1.80000000","0.02570984":"0.02443131","0.02575872":"0.11591332","0.02575989":"0.09399662","0.02579641":"0.75190000","0.02580000":"0.06392453","0.02581814":"0.34347967","0.02582554":"20.00000000","0.02583260":"0.03004038","0.02583580":"0.41763784","0.02585000":"0.03000000","0.02585200":"4.82562511","0.02588000":"699.37000014","0.02588800":"1.00000000","0.02588977":"0.02000000","0.02589000":"0.05000000","0.02589380":"0.05000000","0.02589641":"0.75200000","0.02590000":"6.97837447","0.02591420":"0.11270626","0.02592439":"0.25000010","0.02593801":"0.50000000","0.02595842":"0.22682291","0.02597578":"0.22916250","0.02598346":"0.01876000","0.02599000":"0.10000000","0.02599900":"10.00000000","0.02600000":"38.19875434","0.02601926":"0.01270000","0.02602019":"0.02566884","0.02605141":"0.10000000","0.02606000":"1.00000000","0.02607021":"0.00458189","0.02607063":"0.10958793","0.02607609":"0.02527763","0.02610000":"0.01149426","0.02610165":"0.12733523","0.02611000":"1.33000000","0.02613128":"0.40718911","0.02615000":"1.00000000","0.02617537":"0.18914366","0.02617538":"0.15131493","0.02618080":"0.10000000","0.02618300":"0.96184394","0.02620000":"0.00381680","0.02622799":"0.10655588","0.02628746":"0.01968000","0.02629875":"0.14067708","0.02630000":"0.01140685","0.02633241":"0.02738542","0.02634505":"0.35848783","0.02635000":"0.12319035","0.02635297":"1.50038712","0.02636015":"0.05000000","0.02636679":"0.04266747","0.02638631":"0.10360773","0.02638975":"0.82414691","0.02640000":"0.15000000","0.02640731":"0.75010000","0.02641900":"2.14880491","0.02642590":"0.09617133","0.02644800":"0.05000000","0.02645710":"0.45364582","0.02650000":"3.31519613","0.02650731":"0.75020000","0.02651000":"0.03000000","0.02651179":"0.08054606","0.02652890":"0.04995500","0.02654305":"0.03774159","0.02654558":"0.10074115","0.02656273":"0.00535411","0.02657668":"0.14871273","0.02658913":"0.02068000","0.02659482":"0.09842772","0.02660000":"1.16527949","0.02660731":"0.75030000","0.02663150":"0.00468029","0.02668000":"0.18300000","0.02668421":"0.01000000","0.02668612":"3.99400001","0.02669733":"0.01427223","0.02670000":"0.50000000","0.02670581":"0.09795387","0.02670731":"0.75040000","0.02675000":"1.33289836","0.02677997":"17.00000000","0.02679710":"0.09126439","0.02680000":"0.05609507","0.02680731":"0.75050000","0.02685210":"5.00000000","0.02686060":"0.55695311","0.02686100":"5.00000000","0.02686701":"0.09524371","0.02688271":"0.37349961","0.02688977":"0.02000000","0.02689081":"0.02172000","0.02689886":"0.05000000","0.02690000":"0.50743495","0.02690322":"0.11596188","0.02690584":"0.83413998","0.02690731":"0.75060000","0.02692108":"1.00000000","0.02692439":"0.25000000","0.02695579":"0.90729164","0.02699700":"0.41586666","0.02700000":"63.13214997","0.02700731":"0.75070000","0.02701000":"2.00000000","0.02701926":"0.01276691","0.02702533":"0.10000000","0.02703308":"0.09274765","0.02705141":"0.10000000","0.02705454":"0.05600000","0.02706001":"0.15000000","0.02710000":"2.00000000","0.02710428":"0.05562809","0.02710731":"0.75080000","0.02711000":"1.00000000","0.02712000":"1.51496323","0.02712401":"0.02212063","0.02715000":"0.50000000","0.02717000":"0.03000000","0.02718081":"0.10000000","0.02719248":"0.02280000","0.02720000":"5.50000000","0.02720409":"0.09045269","0.02720731":"0.75090000","0.02723000":"1.99700001","0.02723758":"0.12391620","0.02725000":"1.00000000","0.02725675":"3.21390363","0.02727509":"0.99650250","0.02728000":"9.00000000","0.02730000":"0.04459353","0.02730002":"0.02498750","0.02730731":"0.75100000","0.02732000":"0.05000000","0.02736458":"0.00370277","0.02736830":"0.08795008","0.02739000":"1.00000000","0.02740731":"0.75110000","0.02743134":"0.38851429","0.02749648":"0.02392000","0.02749900":"0.84902282","0.02750000":"0.72623627","0.02750731":"0.75120000","0.02752000":"1.00000000","0.02753026":"0.05625351","0.02753349":"0.08551670","0.02760724":"1.55058834","0.02760731":"0.75130000","0.02767000":"1.00000000","0.02769894":"0.75010000","0.02769969":"0.08315065","0.02770000":"0.50722022","0.02770731":"0.75140000","0.02775311":"0.25149783","0.02777583":"0.10000000","0.02777777":"0.32903348","0.02779815":"0.02512000","0.02779894":"0.75020000","0.02780002":"0.02488750","0.02780731":"0.75150000","0.02781000":"1.00000000","0.02782067":"0.20002484","0.02784380":"0.05000000","0.02785000":"0.03000000","0.02786690":"0.08085006","0.02788800":"1.00000000","0.02788977":"0.02000000","0.02789473":"0.01000000","0.02789520":"7.58055660","0.02789894":"0.75030000","0.02790000":"8.05234495","0.02790731":"0.75160000","0.02792439":"0.25000000","0.02793779":"6.00441683","0.02793899":"33.12016500","0.02794172":"1.20858953","0.02795000":"1.00000000","0.02796308":"0.73084727","0.02796459":"0.49960000","0.02797515":"0.00361034","0.02799117":"0.40353124","0.02799894":"0.75040000","0.02799900":"3.00000000","0.02800000":"91.71497999","0.02800731":"0.75170000","0.02801195":"1.00000000","0.02801918":"0.00998501","0.02803511":"0.07861313","0.02808672":"5.04047013","0.02808700":"1.00000000","0.02809894":"0.75050000","0.02809983":"0.02640000","0.02810000":"0.01087841","0.02810731":"0.75180000","0.02811179":"0.00880000","0.02817226":"2.29316776","0.02818081":"0.10000000","0.02819894":"0.75060000","0.02820000":"0.23581929","0.02820275":"0.01997195","0.02820731":"0.75190000","0.02821247":"0.07666791","0.02823000":"1.00000000","0.02824360":"0.04995501","0.02826560":"0.00518056","0.02826942":"11.30521404","0.02827980":"0.00148286","0.02829894":"0.75070000","0.02830000":"0.14997662","0.02830133":"0.65626553","0.02830235":"0.06492216","0.02830307":"0.05100000","0.02830731":"0.75200000","0.02831000":"2.00000000","0.02832000":"2.00000000","0.02832800":"0.11380644","0.02833591":"0.13225196","0.02833917":"0.11376158","0.02834500":"1.00000000","0.02835000":"0.01562299","0.02837000":"1.00000000","0.02838000":"0.02554615","0.02838540":"0.00399579","0.02838685":"0.07465868","0.02839894":"0.75080000","0.02840000":"0.41236886","0.02840150":"0.02772000","0.02840794":"0.11549974","0.02845000":"2.00000000","0.02846913":"0.11407423","0.02847935":"0.11392010","0.02849894":"0.75090000","0.02849999":"0.50000000","0.02850000":"1.55519463","0.02850865":"0.11329579","0.02855257":"5.19446106","0.02855820":"0.07259305","0.02856242":"0.41854937","0.02856341":"0.01000000","0.02857000":"1.00000000","0.02859000":"2.00000000","0.02859894":"0.75100000","0.02860000":"2.10000000","0.02862340":"0.14683131","0.02863022":"0.09036925","0.02867000":"1.00000000","0.02869894":"0.75110000","0.02870000":"0.53560677","0.02870550":"0.02908000","0.02873560":"2.00000000","0.02873561":"0.11327961","0.02873888":"0.07079680","0.02875179":"0.11523373","0.02877000":"1.00000000","0.02877652":"0.57993828","0.02877699":"3.99459870","0.02878800":"0.05000000","0.02879894":"1.22074954","0.02880000":"0.10000000","0.02884380":"0.05000000","0.02887000":"1.00000000","0.02888080":"2.00000000","0.02888977":"0.02000000","0.02889894":"0.75130000","0.02890000":"0.00692042","0.02891235":"0.06883801","0.02892439":"0.25000000","0.02893170":"0.22000000","0.02897000":"1.00000000","0.02898000":"0.10000000","0.02898970":"2.00000000","0.02898999":"1.50850000","0.02899200":"5.00000000","0.02899894":"0.75140000","0.02899990":"0.50000000","0.02899999":"0.00348276","0.02900000":"39.58254996","0.02900717":"0.03092000","0.02900801":"4.98392820","0.02905233":"11.76652676","0.02907000":"1.00000000","0.02908687":"0.06693342","0.02909894":"0.75150000","0.02910000":"0.30000000","0.02910526":"0.01000000","0.02913127":"0.50019306","0.02914533":"0.43356789","0.02917000":"1.00000000","0.02919894":"0.75160000","0.02920000":"0.05988966","0.02922500":"0.49550000","0.02923000":"0.71185556","0.02926245":"0.06508152","0.02927000":"1.00000000","0.02929530":"0.11106687","0.02929570":"5.64837395","0.02929894":"0.75170000","0.02930000":"0.00682594","0.02930922":"0.08279308","0.02933246":"0.11027274","0.02933306":"0.05752906","0.02934800":"0.04009406","0.02935000":"63.62280623","0.02938990":"133.85550640","0.02939487":"4.00000000","0.02939894":"0.75180000","0.02940000":"0.32840000","0.02945540":"2.00000000","0.02948388":"0.01034125","0.02949000":"1.00000000","0.02949038":"0.08062332","0.02949894":"0.75190000","0.02949900":"0.90000000","0.02949999":"0.50000000","0.02950000":"2.33681337","0.02950271":"0.02995501","0.02955390":"2.00000000","0.02956349":"150.00000000","0.02959000":"1.00000000","0.02959300":"0.10894162","0.02959894":"0.75200000","0.02960000":"0.19968002","0.02960486":"0.16589051","0.02960566":"0.02020101","0.02961260":"0.50000000","0.02961500":"0.98026530","0.02961880":"0.00500000","0.02962200":"0.09990000","0.02965240":"2.00000000","0.02966600":"0.04800000","0.02966839":"0.07839266","0.02968001":"20.00000000","0.02970000":"0.70673401","0.02973501":"1.00000000","0.02974014":"0.44858586","0.02975020":"2.00000000","0.02978270":"0.20066926","0.02980000":"0.27215573","0.02980810":"0.10000000","0.02981000":"20.00000000","0.02984089":"0.40078848","0.02984980":"0.00013407","0.02986037":"0.07656776","0.02987900":"2.00000000","0.02988000":"0.10000000","0.02988512":"0.00337960","0.02988594":"0.64503100","0.02988686":"0.18500000","0.02988800":"1.00000000","0.02988949":"0.25310759","0.02988977":"0.02000000","0.02989569":"0.52000000","0.02990000":"13.50000000","0.02991031":"0.10812416","0.02991430":"0.00437040","0.02991550":"0.40000000","0.02992550":"0.20112207","0.02992883":"0.27109250","0.02993063":"0.40022317","0.02993967":"7.11081548","0.02994000":"0.49000000","0.02995000":"1.00000000","0.02995536":"1.00330087","0.02996003":"0.45353476","0.02996687":"0.40139117","0.02997000":"2.00000000","0.02998197":"0.16239264","0.02998700":"0.00435313","0.02998777":"0.10898917","0.02999065":"0.40138676","0.02999999":"1.74055624","0.03000000":"204.40711041","0.03000839":"0.10841111","0.03001150":"0.00438126","0.03004122":"0.07038661","0.03004493":"0.07456115","0.03004507":"1.99700000","0.03005700":"2.00000000","0.03009002":"0.01536906","0.03010000":"0.00664452","0.03014000":"0.05783801","0.03014800":"2.00000000","0.03015000":"5.00000000","0.03016868":"0.05600000","0.03019064":"0.00548600","0.03023850":"2.00000000","0.03025000":"0.20000000","0.03026405":"1.05472646","0.03029999":"20.48401000","0.03030000":"3.00000000","0.03030329":"2.00000000","0.03031578":"0.01000000","0.03034709":"0.46360209","0.03035698":"3.95196084","0.03036380":"2.00000000","0.03038459":"0.01005016","0.03040000":"3.06044136","0.03040131":"0.40454980","0.03042200":"2.00000000","0.03049999":"0.50000000","0.03050000":"2.62245738","0.03051200":"2.00000000","0.03056576":"0.10700000","0.03056698":"0.07257089","0.03056995":"0.00613604","0.03057450":"0.01534726","0.03057615":"0.00330322","0.03060000":"3.65000000","0.03060290":"2.00000000","0.03062601":"0.17131452","0.03062970":"0.00443008","0.03063950":"0.00443356","0.03064900":"0.00442141","0.03066351":"0.12922011","0.03067651":"0.25064543","0.03070000":"1.75867885","0.03070531":"0.99700004","0.03073199":"1.17671349","0.03073370":"0.01000000","0.03074500":"0.01530000","0.03074501":"0.09000000","0.03074890":"2.00000000","0.03076000":"0.00342490","0.03079999":"0.49920000","0.03080000":"0.12280000","0.03080140":"0.99910000","0.03080810":"0.10000000","0.03081335":"2.02942486","0.03081481":"0.12384400","0.03082000":"0.20000000","0.03084100":"2.00000000","0.03084707":"0.00365249","0.03086000":"0.20000000","0.03086384":"0.00364827","0.03086720":"0.00464773","0.03087453":"3.68300000","0.03088977":"0.02000000","0.03089880":"0.01063939","0.03090000":"0.51522250","0.03092290":"0.00463763","0.03093210":"0.02000000","0.03093400":"2.00000000","0.03094158":"0.06871377","0.03094219":"0.25000000","0.03095000":"1.00000000","0.03095073":"0.07725004","0.03096000":"0.36800788","0.03096642":"0.47861524","0.03097890":"0.00463111","0.03098000":"1.00000000","0.03099999":"0.39968000","0.03100000":"14.94840935","0.03102700":"2.00000000","0.03103756":"0.41352670","0.03104960":"0.00060044","0.03105000":"0.20616528","0.03107020":"0.00463812","0.03108081":"0.10000000","0.03109000":"0.06900000","0.03109467":"0.04980511","0.03110000":"11.27708559","0.03110070":"0.00465742","0.03112600":"0.10700240","0.03112835":"0.06681262","0.03113529":"0.02058114","0.03115955":"0.03500000","0.03116042":"0.02345474","0.03118518":"0.12384400","0.03119500":"2.00000000","0.03120000":"10.05000000","0.03120900":"2.00000000","0.03126300":"0.15492489","0.03127726":"0.99920002","0.03129622":"0.09980000","0.03130000":"0.50638978","0.03131625":"0.06496407","0.03133130":"0.90000000","0.03134500":"1.00000000","0.03135297":"0.01800000","0.03135764":"0.04866540","0.03136500":"2.00000000","0.03137037":"0.12384400","0.03137979":"16.00573953","0.03139048":"0.24431985","0.03139667":"0.08981716","0.03139997":"0.16754240","0.03139999":"0.49920000","0.03141500":"2.00000000","0.03145988":"0.88123247","0.03149000":"2.00000000","0.03150000":"1.22818054","0.03150528":"0.06316666","0.03151001":"0.27172975","0.03152100":"2.00000000","0.03152277":"0.03419886","0.03152392":"0.00348941","0.03152631":"0.01000000","0.03152978":"0.01800000","0.03153951":"2.98500001","0.03155550":"1.38000000","0.03155555":"0.12384400","0.03155793":"0.23429190","0.03157392":"0.18637608","0.03159000":"2.00000000","0.03159014":"0.05000000","0.03159606":"0.00357958","0.03159839":"0.49362427","0.03159999":"0.50000000","0.03160000":"1.49722784","0.03161350":"2.00000000","0.03161434":"0.41120580","0.03162551":"4.31104421","0.03163630":"0.03231663","0.03169546":"0.06141898","0.03170000":"1.56499106","0.03170600":"2.00000000","0.03173377":"0.00500000","0.03174074":"0.12384400","0.03174900":"0.01600000","0.03177260":"0.00454190","0.03179850":"2.00000000","0.03179999":"0.49920001","0.03180000":"0.07508364","0.03184000":"0.22548655","0.03188677":"0.05971966","0.03188977":"0.02000000","0.03189100":"2.00000000","0.03189540":"11.00000000","0.03189820":"0.00452968","0.03190000":"0.18948191","0.03191814":"6.14826113","0.03192592":"0.12384400","0.03192669":"0.53391317","0.03194000":"0.01620029","0.03195000":"9.00000000","0.03197270":"0.00454983","0.03198350":"2.00000000","0.03198585":"0.01837025","0.03199791":"0.02296281","0.03199999":"0.50000000","0.03200000":"40.46331921","0.03200077":"0.02101911","0.03200150":"0.00453696","0.03201389":"0.01681528","0.03201621":"0.02307399","0.03203007":"11.64489377","0.03203119":"0.02884249","0.03205119":"0.04105294","0.03206000":"0.08022936","0.03206680":"0.00455929","0.03207600":"2.00000000","0.03207925":"0.05806735","0.03208081":"0.10000000","0.03209626":"6.62407577","0.03210000":"0.12393053","0.03210400":"0.35648705","0.03211111":"0.12384400","0.03212990":"0.00455220","0.03214950":"0.01621173","0.03215000":"3.00000000","0.03215019":"4.88998999","0.03216850":"2.00000000","0.03217200":"3.00000000","0.03217677":"0.06262125","0.03218543":"0.00313806","0.03220000":"1.15948191","0.03224326":"0.50862791","0.03226100":"2.00000000","0.03226124":"0.06245728","0.03226999":"0.04992500","0.03227288":"0.05646076","0.03229629":"0.12384400","0.03229999":"0.99920002","0.03230000":"0.13016174","0.03231500":"0.10181232","0.03232350":"1.00000000","0.03233200":"9.00000000","0.03235301":"1.22645902","0.03235873":"0.01950000","0.03236400":"0.03784381","0.03237000":"1.17551999","0.03237773":"0.00500000","0.03240000":"0.05440053","0.03242358":"0.44391054","0.03247000":"0.00998500","0.03247139":"0.81743961","0.03247236":"0.05494257","0.03248003":"0.10000000","0.03248148":"0.12384400","0.03248480":"1.00000000","0.03249609":"0.05130784","0.03249999":"0.50000000","0.03250000":"2.72776223","0.03250010":"1.91242999","0.03250500":"0.01986860","0.03253072":"0.00455415","0.03254820":"0.04995500","0.03255000":"9.21692308","0.03257791":"100.00000000","0.03258731":"0.01950000","0.03260000":"0.07273198","0.03261434":"0.32202101","0.03261500":"0.93167959","0.03264610":"1.00000000","0.03265490":"0.10000000","0.03266666":"0.12384400","0.03266837":"0.05338501","0.03268001":"20.00000000","0.03270000":"0.86165430","0.03272512":"2.00000000","0.03273000":"0.87709918","0.03273420":"0.00410021","0.03273609":"1.81400310","0.03273684":"0.01000000","0.03277002":"0.04045133","0.03278000":"0.10000000","0.03279210":"0.01896881","0.03279999":"0.50000000","0.03280000":"0.06100000","0.03280275":"0.05080496","0.03280740":"1.00000000","0.03282238":"0.05000000","0.03282725":"0.06000000","0.03285050":"0.03775729","0.03285185":"0.12384400","0.03286557":"0.05187160","0.03287750":"0.00409271","0.03288900":"0.00304068","0.03288977":"0.02000000","0.03290000":"2.25113663","0.03290129":"0.52362474","0.03292100":"0.02000000","0.03295590":"0.01598209","0.03295922":"74.72829286","0.03296870":"1.00000000","0.03298068":"0.28000000","0.03299000":"0.05201528","0.03299999":"0.50306061","0.03300000":"35.78867286","0.03303703":"0.12384400","0.03306395":"0.05040110","0.03310000":"0.19984000","0.03313000":"1.00000000","0.03315000":"0.09507699","0.03315150":"0.01700000","0.03317500":"0.50000000","0.03320501":"0.04512530","0.03322061":"1.00000000","0.03322222":"0.12384400","0.03323208":"0.12257900","0.03326353":"0.04897228","0.03327773":"0.00400000","0.03328500":"0.00478411","0.03328505":"0.03540625","0.03329319":"0.03627504","0.03329700":"1.00000000","0.03329999":"0.50000000","0.03330000":"1.22047166","0.03332083":"0.01841126","0.03332449":"1.15173328","0.03332521":"0.90334769","0.03333004":"2.00348384","0.03333333":"0.82705092","0.03335000":"0.09985000","0.03336380":"2.00000000","0.03340000":"0.11300000","0.03340740":"0.12384400","0.03342498":"0.00500000","0.03345330":"0.00470040","0.03345689":"1.12500000","0.03346400":"1.00000000","0.03346432":"0.04758397","0.03349000":"4.49493001","0.03349999":"0.50000000","0.03350000":"3.53000000","0.03350980":"0.00472970","0.03353397":"2.99550001","0.03353511":"0.45503231","0.03354840":"0.00473430","0.03357275":"0.53861336","0.03358170":"0.00489105","0.03359259":"0.12384400","0.03360000":"0.50000000","0.03360001":"0.08566423","0.03363000":"1.00000000","0.03365324":"0.22706239","0.03366600":"0.02400000","0.03370000":"0.60593472","0.03370469":"0.06845782","0.03370650":"0.04538200","0.03377777":"0.12384400","0.03379999":"0.50000000","0.03380000":"1.10000000","0.03381000":"0.47870875","0.03381673":"0.05000000","0.03384200":"5.00000000","0.03385030":"0.05556334","0.03385502":"0.00305281","0.03387940":"0.00298116","0.03388977":"0.02000000","0.03389000":"0.16400000","0.03390000":"3.13000000","0.03392800":"0.59940000","0.03394441":"0.03733329","0.03394736":"0.01000000","0.03396296":"0.12384400","0.03397000":"1.00000000","0.03398711":"3.00000000","0.03399990":"3.35194511","0.03399999":"1.00000000","0.03400000":"103.12544213","0.03402165":"0.49850000","0.03403141":"0.07437993","0.03404000":"1.00000000","0.03407040":"0.00833380","0.03407645":"0.00681959","0.03408010":"0.01859400","0.03410000":"2.74420534","0.03411000":"0.06843481","0.03414000":"1.00000000","0.03414814":"0.12384400","0.03420000":"0.50000000","0.03420650":"0.04402000","0.03422061":"1.00000000","0.03422161":"0.22389564","0.03425000":"12.51471917","0.03425615":"0.30000000","0.03425791":"0.55359219","0.03426000":"1.07230130","0.03426051":"0.27813022","0.03427309":"0.47364847","0.03427551":"0.04291725","0.03430000":"1.19342189","0.03433333":"0.12384400","0.03436310":"0.06737000","0.03440000":"4.36000000","0.03442000":"0.94450000","0.03443001":"0.77764291","0.03444431":"0.03586522","0.03448000":"1.00000000","0.03449999":"0.54820793","0.03450000":"3.66531584","0.03450004":"0.12063340","0.03450413":"0.05019924","0.03450921":"0.00650506","0.03451851":"0.12384400","0.03453047":"11.98400003","0.03458010":"0.01779194","0.03460000":"75.49777013","0.03462001":"0.51385904","0.03465000":"0.48559718","0.03465500":"1.00000000","0.03468001":"20.00000000","0.03470000":"50.80000000","0.03470279":"0.28830000","0.03470370":"0.12384400","0.03473200":"0.10000000","0.03475578":"2.12049382","0.03480000":"52.30277541","0.03482600":"1.00301438","0.03484501":"0.00289854","0.03486280":"0.03368778","0.03486500":"6.72163862","0.03487500":"0.10000000","0.03487564":"1.87792834","0.03488888":"0.12384400","0.03490000":"5.94332531","0.03490440":"0.38668244","0.03491020":"0.32855150","0.03493000":"0.00445894","0.03494255":"0.93574136","0.03495000":"0.07500000","0.03495687":"0.00944049","0.03495706":"0.56855993","0.03499044":"0.03695204","0.03499990":"1.06302051","0.03499999":"1.00288572","0.03500000":"149.70992406","0.03501549":"0.12361385","0.03501621":"8.28383943","0.03506639":"0.06427397","0.03506796":"0.31900000","0.03507407":"0.12384400","0.03510000":"0.53490666","0.03511000":"15.00000000","0.03513718":"30.27310778","0.03514009":"25.55638417","0.03514800":"0.01000000","0.03515789":"0.01000000","0.03517540":"1.00000000","0.03518100":"14.30644802","0.03520000":"1.10187552","0.03522001":"0.40000000","0.03522061":"1.00000000","0.03523500":"0.04317220","0.03525925":"0.12384400","0.03530000":"3.55722198","0.03534000":"0.13345133","0.03535130":"1.00000000","0.03536000":"0.30215324","0.03536575":"0.92341756","0.03538110":"0.00296607","0.03538604":"0.04376510","0.03540000":"0.06286459","0.03542000":"0.03036359","0.03542480":"1.31418970","0.03544444":"0.12384400","0.03545035":"0.39923857","0.03550000":"3.53980589","0.03550873":"9.96644629","0.03551000":"0.00295324","0.03552000":"10.02973326","0.03552012":"0.06600000","0.03555000":"0.10000000","0.03555800":"0.01000000","0.03560000":"0.20000000","0.03560019":"0.39434889","0.03561154":"0.05427161","0.03562259":"0.51000000","0.03562962":"0.12384400","0.03566000":"0.02897366","0.03566252":"0.00560812","0.03566979":"9.99200001","0.03567047":"0.58351462","0.03569206":"0.02270530","0.03570000":"0.46332624","0.03574388":"0.02822155","0.03577777":"0.22399978","0.03579706":"25.00000000","0.03580000":"0.20000000","0.03581481":"0.12384400","0.03582555":"0.30000000","0.03584000":"0.18601190","0.03585000":"2.67631495","0.03587001":"8.97530639","0.03588220":"0.82119673","0.03588600":"0.05215000","0.03588977":"0.02000000","0.03590000":"0.54457308","0.03590010":"0.00289746","0.03594409":"0.34000000","0.03594511":"0.25053082","0.03595800":"0.04990000","0.03596410":"0.27805506","0.03596597":"2.50509172","0.03596649":"0.00312074","0.03598717":"3.44132754","0.03599000":"1.00000000","0.03599500":"0.04282398","0.03599995":"1.00000000","0.03600000":"15.09450067","0.03600531":"0.01198948","0.03601745":"0.01198545","0.03605701":"0.05611134","0.03607004":"0.01000000","0.03610000":"0.91242654","0.03614469":"0.00845878","0.03615017":"0.05000000","0.03622001":"0.10000000","0.03622061":"1.00000000","0.03629431":"0.08392500","0.03630000":"170.01772058","0.03633000":"0.10000000","0.03635000":"0.00282971","0.03635800":"0.04995000","0.03636842":"0.01000000","0.03639844":"0.59845482","0.03650000":"2035.65768694","0.03651000":"0.00287157","0.03653469":"9.00000000","0.03656444":"0.50000000","0.03657000":"0.00301970","0.03666000":"0.02300000","0.03668487":"0.03000000","0.03670000":"0.58650531","0.03675001":"0.16582457","0.03676000":"0.18135655","0.03678010":"0.08799400","0.03680000":"0.25000000","0.03681000":"0.00277876","0.03682814":"0.48500000","0.03684575":"14.09748290","0.03685859":"0.00832072","0.03688977":"0.02000000","0.03690000":"2.06183461","0.03691000":"0.00283759","0.03691339":"0.01169454","0.03694501":"0.11540885","0.03699900":"0.30000000","0.03699990":"0.00878938","0.03699999":"0.17183290","0.03700000":"41.28064656","0.03701112":"0.62000000","0.03701358":"1.00000000","0.03710000":"5.89532661","0.03712000":"0.42070003","0.03714127":"0.61337888","0.03720000":"9.92210094","0.03722001":"0.10000000","0.03722061":"1.00000000","0.03723000":"0.10000000","0.03723062":"0.11100000","0.03725000":"0.03183039","0.03730000":"1.46255813","0.03732000":"0.20000000","0.03736500":"0.50997335","0.03737500":"0.30938000","0.03738990":"0.07500000","0.03740000":"0.10100000","0.03745282":"0.50000000","0.03750000":"6.65595028","0.03750873":"6.16878466","0.03752638":"1.00000000","0.03753950":"0.00532772","0.03754890":"0.62397475","0.03755379":"0.08567735","0.03757791":"100.00000000","0.03757894":"0.01000000","0.03760000":"1.01000000","0.03760474":"0.02000000","0.03760869":"0.75996023","0.03763120":"2.00000000","0.03765000":"0.35111988","0.03769277":"0.04111557","0.03770000":"0.03986000","0.03775020":"0.00276985","0.03777548":"0.03154002","0.03780000":"1.49970419","0.03780033":"0.11941955","0.03783670":"0.02767829","0.03786890":"0.10425924","0.03789775":"0.02000000","0.03790000":"0.50000000","0.03792122":"1.00000000","0.03795485":"0.30560000","0.03799133":"5.06359526","0.03800000":"142.91727240","0.03800125":"2.95272043","0.03800502":"0.49920000","0.03801000":"0.00844914","0.03801570":"0.01000000","0.03804650":"0.08000000","0.03805000":"2.90777010","0.03809440":"6.00000000","0.03810000":"0.51049869","0.03812356":"0.04286954","0.03815000":"5.97810497","0.03820000":"5.88999071","0.03822001":"0.10000000","0.03822061":"1.00000000","0.03822983":"14.87000000","0.03823450":"0.14988002","0.03826777":"0.03000000","0.03827495":"0.51422232","0.03828003":"1.00000000","0.03828505":"0.25024163","0.03830000":"10.13054830","0.03830939":"0.00760000","0.03833257":"1.00000000","0.03833820":"0.33908658","0.03834000":"1.00000000","0.03834265":"0.10141571","0.03835373":"9.45664547","0.03838990":"0.03771627","0.03839500":"0.12406458","0.03839710":"18.00000000","0.03840000":"3.27000000","0.03840042":"2.90813410","0.03841000":"3.39349335","0.03844327":"0.01122914","0.03845282":"0.50000000","0.03850000":"6.64053767","0.03851400":"10.37885951","0.03854303":"0.20000000","0.03856811":"0.03085757","0.03858835":"0.45200000","0.03860000":"0.06932995","0.03860150":"0.00279446","0.03865000":"0.61183255","0.03867545":"4.00000000","0.03869928":"0.10998380","0.03870000":"0.15000000","0.03871851":"0.07693840","0.03875969":"0.54514010","0.03877888":"0.79297796","0.03878947":"0.01000000","0.03880000":"0.16389000","0.03886500":"0.50000000","0.03887001":"0.06645618","0.03887970":"4.96504761","0.03888800":"0.60251253","0.03890000":"2.11028278","0.03895000":"0.00631900","0.03895500":"1.00000000","0.03897755":"0.02000000","0.03898442":"0.50000000","0.03899999":"0.47495977","0.03900000":"50.40324017","0.03901000":"3.00000000","0.03908100":"0.81684814","0.03909735":"1.00000000","0.03911000":"0.62332022","0.03911110":"2.07258465","0.03912501":"0.00288099","0.03920000":"0.10000000","0.03921541":"0.20500000","0.03921920":"0.20960000","0.03922001":"0.10000000","0.03922061":"1.00000000","0.03927994":"0.10000000","0.03930000":"2.15262515","0.03931500":"0.03196885","0.03934500":"1.00000000","0.03938990":"0.05228373","0.03945282":"0.50000000","0.03945594":"2.67086607","0.03950000":"40.57220235","0.03951526":"0.00506133","0.03955499":"0.05606743","0.03964000":"0.00283097","0.03964807":"0.00283039","0.03966297":"0.58153855","0.03966600":"0.03000000","0.03966812":"0.43674244","0.03968001":"20.00000000","0.03969000":"8.00641027","0.03970000":"0.01007557","0.03974094":"3.31898897","0.03975143":"0.04764533","0.03978110":"0.05958000","0.03978526":"0.36345210","0.03980000":"21.00000000","0.03985000":"0.00627370","0.03985103":"0.22820876","0.03986400":"0.44197171","0.03986500":"10.03386429","0.03987984":"0.00536700","0.03988800":"0.50000000","0.03988977":"0.02000000","0.03990000":"0.57436424","0.03990787":"0.02749400","0.03993949":"0.00430848","0.03994316":"0.00259213","0.03994501":"1.00000000","0.03995152":"5.33338744","0.03995500":"1.00000000","0.03996000":"0.00280830","0.03998000":"0.16358762","0.03998548":"0.41415159","0.03999000":"1.90963042","0.03999900":"0.26577503","0.03999999":"0.00252500","0.04000000":"262.65472968","0.04000001":"25.09504848","0.04000002":"0.60206640","0.04005502":"0.05000000","0.04007262":"3.10027546","0.04007501":"0.00452357","0.04007816":"0.92581596","0.04008000":"0.00429871","0.04010000":"0.09430000","0.04013000":"0.00254794","0.04017000":"0.00512898","0.04020000":"0.08097802","0.04021017":"0.10262700","0.04021516":"0.02931100","0.04021518":"0.00405800","0.04022018":"0.00305300","0.04023019":"0.25724700","0.04024020":"0.17792500","0.04024027":"0.03685425","0.04024048":"0.60311100","0.04025952":"0.05654701","0.04026523":"0.05436699","0.04027170":"1.00000000","0.04027994":"0.10000000","0.04028012":"0.01000000","0.04028124":"0.01057700","0.04028221":"0.03099500","0.04028224":"0.02707100","0.04029331":"1.00000000","0.04030000":"0.20600000","0.04032154":"0.25000000","0.04032527":"0.01360000","0.04032529":"0.05755100","0.04033000":"1.21777790","0.04033029":"0.00745400","0.04033530":"0.13691500","0.04034471":"0.11814700","0.04034971":"0.02909000","0.04035267":"0.01327300","0.04036200":"2.16882840","0.04036531":"0.01792700","0.04039035":"0.01521800","0.04040000":"5.68243519","0.04040036":"0.08636300","0.04040535":"0.01822500","0.04041000":"0.00264520","0.04041037":"0.00371300","0.04042000":"0.00492048","0.04042038":"0.01900400","0.04043262":"0.02493467","0.04045542":"0.09954300","0.04047000":"0.00614794","0.04050000":"2.01452262","0.04051277":"0.03825360","0.04051548":"0.01499800","0.04052000":"0.00463484","0.04052048":"0.07156000","0.04054303":"0.10000000","0.04055000":"0.03606129","0.04056801":"14.02351003","0.04057330":"0.04968884","0.04057500":"0.75000000","0.04059000":"0.00461939","0.04059010":"0.00714177","0.04060000":"0.04995000","0.04061115":"2.24560296","0.04062000":"0.00492012","0.04064000":"0.00459322","0.04066600":"0.03000000","0.04068400":"0.05690620","0.04069000":"0.00481954","0.04070000":"0.10467619","0.04070010":"0.10000000","0.04073000":"0.00477823","0.04073953":"1.16341069","0.04075000":"0.00275385","0.04080000":"0.05579365","0.04082000":"0.00584018","0.04083836":"10.00000000","0.04084139":"0.03786356","0.04086000":"0.00476264","0.04086680":"0.00600000","0.04087000":"0.00473351","0.04088518":"0.15925331","0.04089000":"0.00603607","0.04095000":"0.00485765","0.04097000":"0.00482279","0.04099000":"0.00481520","0.04099500":"0.02114349","0.04099950":"0.00999000","0.04100000":"90.49987653","0.04103000":"0.00479142","0.04103726":"0.20000000","0.04104000":"0.25471758","0.04105000":"0.00483853","0.04106000":"0.00479912","0.04108500":"6.58288790","0.04110000":"3.89891824","0.04110815":"4.69309065","0.04113288":"0.00243358","0.04117000":"0.00464300","0.04120000":"11.30000000","0.04122000":"0.00901005","0.04122001":"0.04709793","0.04123634":"0.00388243","0.04127170":"1.00000000","0.04127994":"0.10000000","0.04130000":"0.00968524","0.04133687":"0.10000000","0.04135001":"0.02000000","0.04140000":"0.09651772","0.04141000":"0.00529806","0.04143592":"0.66400000","0.04144000":"0.00535747","0.04147353":"0.05777503","0.04150000":"1.55389277","0.04151870":"0.07305173","0.04155000":"0.01029603","0.04155110":"12.14738210","0.04156952":"0.03463708","0.04158400":"2.00000000","0.04158999":"0.07235500","0.04159002":"0.11996800","0.04159014":"0.06881586","0.04159501":"0.00985694","0.04160000":"0.20541076","0.04161631":"0.00347226","0.04163000":"0.00564181","0.04166000":"0.00573682","0.04170000":"1.00000000","0.04172000":"0.24029300","0.04175000":"0.15000000","0.04176911":"0.00267063","0.04180000":"0.34030767","0.04181000":"0.49221200","0.04187000":"0.00561758","0.04188518":"0.20000000","0.04189644":"0.07056179","0.04189775":"0.02000000","0.04189999":"0.20000000","0.04190000":"0.04995000","0.04190456":"0.24657238","0.04191706":"0.00266292","0.04192277":"0.03696701","0.04196000":"0.00564954","0.04197561":"0.00264413","0.04200000":"40.83636553","0.04202000":"0.02997000","0.04203726":"0.20000000","0.04205044":"5.17629599","0.04210000":"0.00950119","0.04212999":"0.99950000","0.04213060":"0.42108427","0.04214000":"0.00602497","0.04215000":"0.00602746","0.04215139":"0.03590102","0.04216000":"0.00438466","0.04217000":"0.00599074","0.04220000":"0.25694682","0.04221000":"0.15183316","0.04222001":"0.32840000","0.04223000":"0.00594816","0.04225119":"0.10000000","0.04227170":"1.00000000","0.04230000":"0.12997000","0.04232000":"0.00586094","0.04232001":"0.03000000","0.04233000":"0.00433633","0.04234500":"1.00000000","0.04234637":"7.42155222","0.04237000":"0.00576154","0.04237600":"13.19954408","0.04248540":"1.93059722","0.04250000":"1.97750000","0.04251762":"0.02154667","0.04253951":"7.00000000","0.04257500":"0.51187289","0.04262000":"0.15642109","0.04264500":"13.30696071","0.04264562":"0.08903327","0.04268540":"0.00235000","0.04275000":"0.00266310","0.04276521":"0.00720165","0.04278000":"1.23431400","0.04279000":"0.00431358","0.04280000":"0.08840000","0.04280501":"5.00000000","0.04281587":"11.67791288","0.04285001":"0.00351925","0.04287952":"0.02572755","0.04288286":"0.01006661","0.04288518":"0.10010272","0.04290000":"0.53130001","0.04291213":"0.01005975","0.04293000":"0.00618382","0.04293318":"0.24066483","0.04294000":"0.00616972","0.04294890":"0.12082001","0.04298213":"2.99358081","0.04299000":"0.02975875","0.04299990":"0.50000000","0.04299999":"0.00234884","0.04300000":"51.68620095","0.04301764":"9.36181311","0.04302000":"0.00615043","0.04303000":"0.00599452","0.04303726":"0.20000000","0.04304000":"0.14600000","0.04305000":"0.00611895","0.04308999":"0.05405090","0.04309781":"0.21707959","0.04310869":"2.00000000","0.04311110":"0.50971287","0.04312000":"0.00588898","0.04313007":"0.00874874","0.04315000":"0.00593309","0.04318000":"0.00426272","0.04319503":"4.27395281","0.04322135":"0.46650524","0.04324739":"0.09155089","0.04325000":"0.00555610","0.04325010":"1.14821566","0.04326854":"0.00232000","0.04327170":"0.80000000","0.04330000":"0.00551464","0.04334000":"0.00541327","0.04336000":"0.00557347","0.04339070":"2.83872959","0.04340000":"152.72836484","0.04344000":"0.00524405","0.04345583":"0.24149649","0.04346000":"0.11938289","0.04350000":"1.75000000","0.04350275":"0.39175704","0.04350967":"0.02966281","0.04353000":"0.00424783","0.04355010":"3.06855044","0.04355250":"0.99900000","0.04356000":"0.15304561","0.04356695":"2.53238883","0.04357008":"2.69114180","0.04359000":"0.00266293","0.04360000":"4.70183486","0.04360929":"1.79373271","0.04362000":"0.00539013","0.04363000":"0.00540146","0.04364000":"0.00573105","0.04365000":"0.00575250","0.04367000":"0.00573514","0.04367476":"0.00231254","0.04368001":"20.00000000","0.04369000":"0.00552536","0.04370000":"0.01495187","0.04374000":"0.00576489","0.04374960":"1.20239223","0.04375000":"0.00431223","0.04376000":"0.00538453","0.04377000":"1.00425007","0.04378000":"0.00572569","0.04379000":"0.00553573","0.04380000":"0.00540016","0.04382000":"0.00560844","0.04384500":"1.25572435","0.04387690":"0.07194705","0.04389000":"0.00425595","0.04389444":"0.19074018","0.04389775":"0.02000000","0.04390000":"0.17234000","0.04392187":"0.51780021","0.04395000":"0.00544224","0.04398000":"0.00556427","0.04399000":"0.00547927","0.04400000":"14.77235748","0.04403726":"0.20000000","0.04405181":"1.22580338","0.04407571":"0.49800000","0.04410000":"0.08570000","0.04410001":"0.24260000","0.04412228":"11.51172610","0.04413000":"0.00672367","0.04416000":"0.22644400","0.04418952":"0.03397538","0.04420377":"0.01000000","0.04426499":"0.26834838","0.04426854":"0.00226000","0.04427170":"0.80000000","0.04427994":"0.10000000","0.04429000":"0.00671702","0.04429393":"0.49900000","0.04440000":"0.10163433","0.04450000":"1.50898877","0.04453475":"0.01133946","0.04454383":"0.04500000","0.04456000":"0.00557594","0.04462000":"0.00559911","0.04466600":"0.03000000","0.04469000":"0.00541561","0.04472000":"0.00553695","0.04477000":"0.00548803","0.04477272":"111.67512011","0.04479329":"33.93179783","0.04480000":"0.50544745","0.04490000":"1.30387816","0.04490973":"1.00000000","0.04493000":"0.00651896","0.04494000":"0.00656694","0.04496000":"0.00655056","0.04497000":"2.50334908","0.04497661":"0.08000000","0.04498000":"0.00649353","0.04499999":"0.00244444","0.04500000":"50.76650060","0.04503768":"0.00258794","0.04510046":"0.03594967","0.04515000":"0.11790900","0.04525000":"0.00651953","0.04526854":"0.00221000","0.04527170":"0.80000000","0.04528385":"0.00483799","0.04530000":"0.00883003","0.04532000":"0.00662040","0.04534148":"0.00310398","0.04535000":"0.00641503","0.04537501":"0.35000000","0.04539000":"0.00657631","0.04540000":"0.08320000","0.04544001":"0.00649194","0.04549952":"0.03794177","0.04550000":"1.51266087","0.04557000":"0.00652518","0.04559000":"0.25634340","0.04560000":"0.00646346","0.04563800":"0.11987370","0.04565000":"0.00648532","0.04565973":"9.02687975","0.04568001":"20.00000000","0.04572000":"0.00645935","0.04574000":"0.00227572","0.04579000":"0.60430172","0.04587262":"1.26683478","0.04589775":"0.02000000","0.04590000":"1.00000000","0.04590020":"1.00000000","0.04598899":"0.29584445","0.04599094":"0.00260920","0.04600000":"8.27575330","0.04607000":"0.00390278","0.04610000":"0.00867679","0.04610799":"0.20749839","0.04625010":"0.05498233","0.04626854":"0.00217000","0.04627994":"0.10000000","0.04629338":"2.73010126","0.04640000":"0.21551724","0.04646499":"0.04657911","0.04650000":"1.01500000","0.04654440":"0.50000000","0.04658014":"1.05821384","0.04660503":"1.09539473","0.04665010":"10.00000001","0.04670000":"0.50500000","0.04671065":"0.18617842","0.04680000":"0.08080000","0.04687362":"0.12811675","0.04690000":"0.00852879","0.04693000":"0.01413893","0.04697000":"0.04504425","0.04700000":"13.76893819","0.04702001":"24.32266633","0.04704500":"0.15000000","0.04711190":"0.02235105","0.04720000":"1.00000000","0.04720170":"0.30968276","0.04725000":"0.22882595","0.04726499":"0.26834838","0.04726854":"0.00212000","0.04735801":"0.00278570","0.04745900":"0.00898652","0.04747019":"0.02191354","0.04750000":"5.06552640","0.04750058":"1.54591811","0.04750059":"7.19672456","0.04753000":"0.01115027","0.04757791":"99.00000000","0.04768001":"20.00000000","0.04770000":"0.00838575","0.04774104":"1.00122607","0.04780000":"1.08563357","0.04789775":"0.02000000","0.04790000":"1.60000000","0.04791240":"36.11112151","0.04797854":"0.00861690","0.04798083":"0.30000000","0.04799999":"0.00210417","0.04800000":"13.32205328","0.04807571":"0.50000000","0.04810000":"0.07860000","0.04822995":"2.08441498","0.04824000":"0.47703241","0.04826854":"0.00208000","0.04827519":"0.00211735","0.04830000":"1.71959797","0.04832921":"0.03986890","0.04839072":"0.00240969","0.04839764":"5.45361901","0.04844000":"0.00229770","0.04848000":"8.54119545","0.04850000":"1.00824743","0.04852957":"0.07826151","0.04862286":"0.43950000","0.04868001":"20.00000000","0.04870148":"45.86231136","0.04874001":"0.00559094","0.04883238":"0.10000000","0.04887713":"0.09149646","0.04888000":"2.57003081","0.04889775":"0.02000000","0.04889949":"0.00226996","0.04890000":"5.26976751","0.04890925":"0.04309282","0.04892042":"0.21287721","0.04892681":"0.65231645","0.04900000":"43.99408869","0.04914000":"0.10000000","0.04925000":"0.00499500","0.04926499":"0.26834838","0.04926854":"0.00203000","0.04930000":"0.00811360","0.04934500":"1.00000000","0.04935001":"0.22019537","0.04940000":"0.64735021","0.04941394":"1.79327365","0.04945655":"6.99300001","0.04947572":"6.86786879","0.04948000":"0.20262400","0.04948816":"101.03424718","0.04950000":"2.20870871","0.04955271":"0.17062101","0.04958949":"8.77262189","0.04962261":"0.02416002","0.04965000":"1.99800000","0.04968001":"50.00000000","0.04975000":"0.80000000","0.04976148":"5.00000000","0.04980601":"0.03000000","0.04982360":"0.05000000","0.04985871":"0.58695558","0.04988040":"6.19864928","0.04988041":"0.00191741","0.04990000":"0.00592988","0.04990973":"1.00000000","0.04998559":"1.00000000","0.04999998":"0.50000000","0.04999999":"0.00220000","0.05000000":"623.94872426","0.05000010":"5.00000000","0.05002344":"33.55321794","0.05004102":"0.30088465","0.05010000":"5.00798404","0.05020000":"0.44000000","0.05023997":"35.49852904","0.05025000":"0.00499500","0.05040000":"0.06633000","0.05044915":"0.02551644","0.05050000":"2.00000000","0.05078769":"0.00416286","0.05080000":"0.07450000","0.05090000":"0.00785855","0.05093006":"1.70526598","0.05096000":"0.19625200","0.05098077":"0.00797889","0.05100000":"12.00895221","0.05102248":"0.00197952","0.05103000":"0.05908895","0.05110000":"0.29354207","0.05111880":"0.03500000","0.05113000":"0.19608700","0.05113298":"1.24051242","0.05120000":"0.04987500","0.05125000":"0.00499500","0.05127500":"0.01000000","0.05131086":"0.03436851","0.05139452":"3.00000000","0.05145000":"0.09424800","0.05147000":"0.42230000","0.05150000":"2.00000000","0.05152000":"10.00000000","0.05160369":"0.16186633","0.05166009":"0.03333000","0.05170000":"0.00773695","0.05175649":"0.40000000","0.05187600":"3.00000000","0.05189775":"0.01000000","0.05194280":"0.05000000","0.05200000":"65.21911303","0.05200890":"0.05000000","0.05205640":"0.03000000","0.05210000":"0.27260000","0.05220065":"1.00000000","0.05224090":"0.05000000","0.05225000":"0.00499500","0.05225446":"0.30000000","0.05227780":"0.05000000","0.05228503":"0.05565445","0.05232220":"0.05000000","0.05242449":"0.00914338","0.05243573":"0.00924425","0.05243710":"0.12000000","0.05244226":"0.01172868","0.05250000":"4.20761905","0.05250001":"0.20698366","0.05253500":"0.00211287","0.05257500":"0.10184500","0.05265838":"0.01944324","0.05270400":"2.80387332","0.05290000":"1.50000000","0.05299050":"4.00330139","0.05299999":"0.00190566","0.05300000":"7.82090767","0.05311350":"0.00357683","0.05314051":"0.77926871","0.05320000":"1.00610497","0.05325000":"0.00499500","0.05330000":"0.00750470","0.05340000":"0.07790319","0.05345000":"1.00000000","0.05350000":"2.00000000","0.05355000":"0.14988600","0.05367004":"0.30000000","0.05368001":"50.00000000","0.05371000":"5.22995159","0.05375000":"1.01996623","0.05376000":"0.18601190","0.05385871":"1.00000000","0.05389775":"0.01000000","0.05390000":"22.00000000","0.05394540":"1.27837024","0.05396000":"0.12354831","0.05400000":"18.09283506","0.05410000":"0.00739372","0.05417867":"0.36143386","0.05422000":"5.00000000","0.05425000":"0.00499500","0.05431536":"0.10000000","0.05432000":"30.00000000","0.05433907":"1.35093135","0.05436935":"0.21695347","0.05440894":"0.03468148","0.05442800":"0.10000000","0.05448849":"91.76248200","0.05450000":"2.00000000","0.05460000":"0.20512900","0.05475001":"0.08092734","0.05480000":"0.06900000","0.05483967":"2.07292501","0.05487900":"5.14939613","0.05488001":"0.00189175","0.05489000":"0.00550000","0.05490000":"0.00728598","0.05490973":"1.00000000","0.05491000":"0.20000000","0.05493311":"0.56761375","0.05498300":"0.38776097","0.05499999":"0.00200000","0.05500000":"53.67772757","0.05501111":"0.18550774","0.05501621":"8.00000000","0.05502121":"0.10000000","0.05514000":"0.18135655","0.05525000":"0.00499500","0.05546800":"0.00200115","0.05550000":"2.00000000","0.05550058":"0.02126410","0.05560000":"0.10815774","0.05565000":"0.19146227","0.05568827":"0.02032539","0.05570000":"0.00718133","0.05580050":"0.08000000","0.05583001":"0.00187410","0.05589775":"0.01000000","0.05590000":"0.00179998","0.05598000":"1.00000000","0.05600000":"11.65382245","0.05610000":"0.06740000","0.05613436":"0.01799519","0.05620000":"0.26690391","0.05620610":"0.03333002","0.05621000":"0.60000000","0.05621800":"0.04990000","0.05625000":"0.00499500","0.05625871":"0.97407195","0.05627000":"1.99800000","0.05633317":"45.15937053","0.05640601":"1.37380045","0.05650000":"2.00707965","0.05660600":"0.17965687","0.05665851":"1.50000000","0.05666000":"1.00000000","0.05668001":"20.00000000","0.05670000":"9.17800754","0.05680680":"0.03000000","0.05696549":"0.00196996","0.05700000":"5.15086383","0.05700006":"0.10000000","0.05707070":"52.38800632","0.05712356":"0.33428820","0.05712459":"0.68900000","0.05720000":"0.00355768","0.05721001":"0.10000000","0.05725000":"0.00499500","0.05726499":"0.25711050","0.05726664":"0.54782086","0.05730000":"0.00698081","0.05739099":"0.33729192","0.05740000":"0.06580000","0.05746625":"0.12149266","0.05750000":"3.30000000","0.05754505":"1.00000000","0.05763796":"0.36125844","0.05770600":"0.00182762","0.05772900":"0.38969621","0.05774670":"0.05000000","0.05775000":"0.18449999","0.05776148":"5.00000000","0.05777000":"0.07752425","0.05777306":"0.21858755","0.05781024":"0.05237000","0.05789000":"0.00172801","0.05789775":"0.01000000","0.05790000":"0.50000000","0.05797000":"0.00173000","0.05800000":"74.78375794","0.05800010":"0.07558331","0.05800255":"0.35313881","0.05810000":"0.00688469","0.05824870":"0.05006427","0.05824995":"4.42343654","0.05825000":"0.00499500","0.05830000":"0.01034762","0.05836256":"0.24000000","0.05841502":"3.92162399","0.05842070":"0.00365429","0.05850000":"2.07985000","0.05853425":"0.05500000","0.05860000":"0.07050776","0.05865000":"0.06304640","0.05866210":"0.83641742","0.05870000":"0.06430000","0.05877981":"0.25804059","0.05880000":"0.70000000","0.05880001":"0.18120535","0.05883026":"0.01536200","0.05883239":"0.00188671","0.05883325":"0.09680440","0.05883711":"2.00000000","0.05887770":"0.00298030","0.05889000":"0.13994000","0.05889775":"0.01000000","0.05890000":"0.23006425","0.05890560":"0.00175444","0.05900000":"4.11891611","0.05900722":"2.50000000","0.05907900":"0.49950000","0.05910485":"0.82209997","0.05917316":"0.25496430","0.05918500":"0.51003060","0.05921001":"0.10000000","0.05921160":"0.05000000","0.05922080":"0.12000000","0.05925000":"0.00499500","0.05932401":"0.04503634","0.05934500":"1.00000000","0.05936751":"0.93112807","0.05940601":"1.00000000","0.05942000":"0.05241696","0.05948844":"84.04992902","0.05950000":"2.00000000","0.05957999":"0.03320011","0.05958007":"16.28454810","0.05960000":"0.01724116","0.05960200":"0.33555920","0.05970000":"0.00670017","0.05976000":"1.17198906","0.05984428":"0.10000000","0.05985001":"0.17802631","0.05985997":"0.35880093","0.05988000":"1.28034501","0.05990000":"0.10000000","0.05990973":"1.00000000","0.05991000":"4.00775618","0.05993341":"0.04144024","0.05993727":"0.00172173","0.05995228":"0.05500000","0.05997604":"0.32787423","0.05999000":"22.72140037","0.05999999":"0.00183333","0.06000000":"54.19085476","0.06000006":"0.10000000","0.06000990":"0.10000000","0.06010000":"2.15940000","0.06020000":"0.51866984","0.06021000":"0.39700000","0.06025000":"0.00499500","0.06030842":"0.02981504","0.06040601":"1.00000000","0.06042880":"0.05045561","0.06043977":"2.66974799","0.06050000":"3.00661158","0.06061059":"2.69698865","0.06090000":"0.27495690","0.06100000":"5.33952572","0.06100461":"0.17882284","0.06100990":"0.10000000","0.06101041":"0.51971238","0.06106080":"0.49388003","0.06108010":"0.00181728","0.06113000":"0.11086113","0.06121001":"0.10000000","0.06122608":"0.03500000","0.06123000":"1.00000000","0.06125000":"0.00499500","0.06130000":"0.00652529","0.06131000":"0.20000000","0.06132000":"0.10871929","0.06133304":"2.00000000","0.06139452":"3.33031825","0.06140000":"0.06160000","0.06150000":"3.00000000","0.06152000":"10.00000000","0.06156290":"0.02500000","0.06160980":"0.03000000","0.06163213":"0.03398545","0.06165843":"0.00538132","0.06181440":"0.05000000","0.06182999":"0.00868356","0.06188000":"0.56000000","0.06189775":"0.01000000","0.06190000":"0.76155088","0.06193525":"0.64204466","0.06195000":"0.17199152","0.06199900":"0.49950000","0.06200000":"19.23409985","0.06200599":"0.10000000","0.06205000":"0.12977976","0.06208000":"0.10738832","0.06210000":"0.00644123","0.06215366":"0.24420526","0.06220065":"1.00000000","0.06225000":"0.00499500","0.06225380":"0.05010956","0.06230261":"0.34742239","0.06234174":"0.57718976","0.06247019":"0.01685263","0.06249001":"0.16036663","0.06250000":"3.05000000","0.06260000":"0.11830452","0.06260420":"0.04044921","0.06270000":"0.06020000","0.06287400":"5.00000000","0.06289044":"0.22300000","0.06290000":"0.10635931","0.06296288":"0.02057107","0.06296500":"5.57621143","0.06300000":"3.43424845","0.06300500":"0.10000000","0.06302563":"0.00178054","0.06310000":"0.00303664","0.06315700":"0.00996539","0.06319454":"0.42907969","0.06320408":"0.20000006","0.06322916":"0.00159736","0.06325000":"0.00499500","0.06340500":"0.10011370","0.06342042":"0.15933976","0.06343999":"0.45849013","0.06347000":"0.08008164","0.06350000":"3.00000000","0.06353000":"0.00174000","0.06354001":"0.10679310","0.06354002":"2.32242013","0.06355000":"0.00161144","0.06363000":"0.35060012","0.06367004":"0.30423434","0.06370000":"2.00627944","0.06373497":"0.22412296","0.06376003":"2.88591936","0.06382851":"0.01001000","0.06389775":"0.01000000","0.06390000":"0.10000000","0.06393000":"0.15642109","0.06394869":"0.00266365","0.06395000":"2.00309133","0.06397000":"0.00157000","0.06400000":"31.21401894","0.06409500":"0.10000000","0.06410000":"0.05900000","0.06412578":"0.64900002","0.06415000":"0.00173031","0.06420000":"0.20000000","0.06424440":"0.01650000","0.06425000":"0.00499500","0.06431536":"0.10000000","0.06439500":"0.50000000","0.06440493":"5.32420989","0.06443993":"0.44890732","0.06444320":"0.05000000","0.06450000":"3.00620156","0.06457500":"0.00158587","0.06459001":"0.11815540","0.06460000":"0.04300966","0.06480000":"40.00000000","0.06488594":"77.05828458","0.06490000":"0.50000000","0.06490973":"1.00000000","0.06491582":"0.00159286","0.06499999":"0.00169231","0.06500000":"18.30260934","0.06500722":"5.00000000","0.06510001":"0.16366935","0.06517281":"0.00172188","0.06524150":"0.08000000","0.06525000":"0.00499500","0.06526020":"0.15300000","0.06529180":"0.05400000","0.06530000":"0.00612558","0.06532500":"0.25220000","0.06534000":"0.15304561","0.06536000":"0.80000000","0.06537380":"0.65813743","0.06540000":"0.05780000","0.06540536":"0.16328183","0.06550000":"3.08101829","0.06560000":"0.00156109","0.06584696":"0.10068998","0.06589775":"0.01000000","0.06590000":"0.10000000","0.06598461":"0.01700000","0.06598833":"0.05500000","0.06600000":"10.05053030","0.06610000":"0.00605144","0.06625000":"0.00499500","0.06630000":"0.20000000","0.06640425":"0.23073083","0.06644000":"0.00170000","0.06645502":"0.00167030","0.06650000":"3.00000000","0.06654696":"0.10000000","0.06655202":"0.00315542","0.06656455":"2.00000000","0.06662500":"0.00153707","0.06665600":"0.13951544","0.06667756":"0.05000000","0.06668001":"20.00000000","0.06670000":"0.05660000","0.06690000":"0.00597908","0.06697000":"0.00150000","0.06698000":"0.00325252","0.06700000":"5.93156280","0.06700001":"0.10076795","0.06700006":"0.10000000","0.06700743":"0.13794885","0.06703569":"0.37146068","0.06705999":"0.19950000","0.06708987":"0.90000000","0.06714082":"0.06153801","0.06720001":"0.15855469","0.06725000":"0.00499500","0.06733436":"0.04228580","0.06750000":"3.00000000","0.06760000":"0.30000000","0.06765000":"0.00151378","0.06770000":"0.00590842","0.06779999":"0.03199801","0.06786872":"0.02012214","0.06788000":"0.99900000","0.06789775":"0.01000000","0.06790000":"0.20000000","0.06794000":"0.14718900","0.06796148":"5.00000000","0.06798426":"0.10000000","0.06800000":"9.88486766","0.06801500":"0.10055622","0.06810000":"0.20234287","0.06811890":"0.01800000","0.06812001":"0.52721658","0.06815944":"1.16605483","0.06821000":"7.17103625","0.06821060":"0.08100000","0.06825000":"0.00499500","0.06825480":"0.05400000","0.06827490":"0.09000000","0.06832013":"0.00164256","0.06835619":"0.00934300","0.06840000":"0.00146361","0.06842000":"0.29970001","0.06850000":"3.50583942","0.06850248":"0.60008935","0.06866108":"0.20395209","0.06867500":"0.00149119","0.06877501":"0.00161395","0.06880000":"50.24000000","0.06889000":"0.10000000","0.06889775":"0.01000000","0.06890000":"20.11055041","0.06898000":"0.00150415","0.06900000":"19.30139885","0.06905500":"0.10000000","0.06909250":"0.49960000","0.06914000":"0.15261512","0.06920070":"0.05000000","0.06925000":"0.00499500","0.06930000":"0.15952201","0.06940000":"0.05450000","0.06941400":"0.19560000","0.06943000":"0.07482924","0.06948859":"71.95424862","0.06950000":"3.35164684","0.06958000":"1.99757499","0.06960690":"0.01684427","0.06964700":"0.00148363","0.06966514":"0.05930817","0.06969000":"0.00148216","0.06970000":"2.65945387","0.06972401":"0.05000000","0.06980000":"1.91367419","0.06980126":"0.44078871","0.06987173":"0.20671023","0.06990000":"0.35250000","0.06990973":"1.00000000","0.06991876":"1.00000000","0.06992001":"0.10064610","0.06995654":"0.03722699","0.06997000":"0.00143000","0.06999999":"0.00157143","0.07000000":"49.97635910","0.07000006":"0.10000000","0.07000070":"1.00000000","0.07005999":"0.30000000","0.07010000":"5.00570614","0.07011855":"0.30000000","0.07013725":"0.00152656","0.07021257":"0.00159828","0.07024855":"0.94940000","0.07025000":"0.00499500","0.07025860":"0.00157317","0.07046000":"0.07375417","0.07049317":"0.01250112","0.07050000":"4.50600000","0.07056500":"0.00285661","0.07070000":"1.11253379","0.07072500":"0.00147779","0.07072760":"1.17672980","0.07078750":"0.00141562","0.07083999":"0.21650888","0.07090000":"0.00564175","0.07100000":"14.73942000","0.07100111":"0.10015757","0.07110250":"0.27944000","0.07112887":"0.00141050","0.07116780":"0.12000000","0.07124380":"0.02575386","0.07125000":"0.00499500","0.07131691":"0.00164358","0.07140001":"0.14922794","0.07144996":"0.03992000","0.07150000":"4.00000000","0.07152000":"10.00000000","0.07152887":"0.00140400","0.07153000":"0.05442360","0.07159412":"5.00000000","0.07159990":"0.05840542","0.07160470":"0.04033339","0.07161470":"0.00139700","0.07163617":"14.00000000","0.07164071":"3.63445895","0.07168000":"0.18601190","0.07170000":"0.00557881","0.07173021":"0.12666575","0.07175000":"0.00151733","0.07187459":"0.05748502","0.07188270":"0.28000000","0.07189540":"10.00000000","0.07189775":"0.01000000","0.07190000":"0.10000000","0.07190511":"0.10000000","0.07195513":"0.29905050","0.07198000":"1.57296624","0.07200000":"59.88840653","0.07201900":"0.10000000","0.07202601":"0.03030602","0.07206110":"0.05052386","0.07210000":"0.24618238","0.07216000":"0.00155514","0.07220065":"1.00000000","0.07222887":"0.00139000","0.07224324":"0.15226333","0.07225000":"0.00499500","0.07227600":"1.06729800","0.07232595":"0.01536996","0.07235273":"0.00629172","0.07249000":"0.00141207","0.07250000":"832.99431180","0.07252555":"0.30000000","0.07254000":"0.00289567","0.07259002":"0.00152913","0.07260000":"0.03642496","0.07261470":"0.00137800","0.07262003":"0.02000000","0.07264700":"0.00142064","0.07265700":"0.00138375","0.07270000":"0.20000000","0.07277500":"0.00143617","0.07278000":"0.08933230","0.07280719":"0.00151647","0.07284101":"0.14025049","0.07288500":"0.30000000","0.07290500":"0.10055764","0.07295130":"1.06039050","0.07299999":"0.00138000","0.07300000":"98.58329535","0.07301000":"0.07060378","0.07306208":"0.05655070","0.07311450":"0.00138148","0.07316375":"0.39475665","0.07320000":"2.99700000","0.07324000":"0.42490749","0.07325000":"0.00499500","0.07330000":"0.46223472","0.07331212":"0.01033522","0.07340000":"0.05150000","0.07342781":"0.00680940","0.07345000":"1.00000000","0.07348971":"0.40000000","0.07350000":"13.78282333","0.07350001":"0.14496428","0.07352000":"0.22723492","0.07354000":"0.15000000","0.07355599":"0.10026934","0.07356000":"2.56282554","0.07360226":"1.38600000","0.07361470":"0.00135900","0.07366000":"0.02500000","0.07367004":"0.10000000","0.07370000":"0.61000000","0.07380000":"0.06138763","0.07389775":"0.01000000","0.07390240":"0.30000000","0.07393855":"0.54306550","0.07399990":"0.99900001","0.07400000":"10.74083951","0.07400499":"0.10000000","0.07405265":"0.01085199","0.07409400":"0.29320565","0.07410000":"0.00539812","0.07413168":"0.00161874","0.07414500":"0.00135000","0.07422564":"0.00843152","0.07425000":"0.00499500","0.07427064":"0.99061302","0.07431536":"0.10000000","0.07432001":"6.91924985","0.07433001":"1.01898000","0.07443524":"0.10020679","0.07443993":"0.33000000","0.07450000":"4.00000000","0.07454000":"0.04461416","0.07455700":"0.00134800","0.07460000":"0.01000000","0.07461470":"0.00134100","0.07470000":"0.05060000","0.07472000":"6.11913003","0.07475010":"0.02005722","0.07479317":"0.01139459","0.07480000":"0.01363636","0.07488694":"66.76730726","0.07490000":"0.25534046","0.07493000":"0.00155806","0.07498977":"0.10464268","0.07499999":"0.00146667","0.07500000":"36.56973740","0.07521363":"5.10873355","0.07525000":"0.00499500","0.07540000":"3.88612894","0.07545700":"0.00133479","0.07549990":"0.00139789","0.07550000":"4.29153739","0.07553370":"0.01196432","0.07553500":"0.20010380","0.07555133":"0.07576459","0.07555421":"0.01861188","0.07556000":"0.06876687","0.07560000":"0.14093750","0.07561470":"0.00132300","0.07567425":"0.32507561","0.07570000":"0.00528402","0.07572500":"0.07377868","0.07577180":"0.05452836","0.07588500":"0.02097139","0.07589775":"0.01000000","0.07592772":"0.10000000","0.07600000":"11.09499999","0.07601000":"0.00300706","0.07601248":"0.48835117","0.07602599":"0.00146002","0.07610000":"0.04970000","0.07612848":"0.66000000","0.07620000":"0.04323377","0.07622257":"0.01500000","0.07625000":"0.04499500","0.07627423":"0.01256253","0.07630000":"0.15000000","0.07632001":"0.53048691","0.07640000":"0.03655810","0.07650000":"4.10529631","0.07651598":"0.14338129","0.07656455":"1.00000000","0.07658000":"0.06800232","0.07658470":"1.00000000","0.07661470":"0.00130600","0.07668001":"20.00000000","0.07670000":"0.05276861","0.07675000":"0.05000000","0.07678086":"0.03335942","0.07680660":"0.05000000","0.07690000":"0.64900000","0.07698077":"0.10000000","0.07700000":"6.04818991","0.07700211":"2.99320660","0.07701475":"0.01319066","0.07709000":"0.05011260","0.07724961":"0.20028389","0.07725000":"0.00499500","0.07729000":"0.25762582","0.07730000":"0.00517465","0.07736815":"0.61594493","0.07737996":"5.45033036","0.07740000":"0.08907441","0.07745000":"0.05000000","0.07750000":"4.10000000","0.07751793":"0.09980000","0.07758050":"0.03932103","0.07760000":"0.11695562","0.07761470":"0.00128900","0.07763003":"0.15811249","0.07767800":"0.70805118","0.07770000":"5.41527328","0.07770001":"0.13712837","0.07775528":"0.01385019","0.07777777":"5.40599317","0.07780000":"0.05000000","0.07781691":"0.02581077","0.07788752":"1.30269600","0.07789332":"0.05304322","0.07789775":"0.01000000","0.07789894":"0.14977500","0.07790000":"4.13962500","0.07790240":"0.30000000","0.07792001":"0.05006311","0.07793988":"0.00143982","0.07794180":"0.01500000","0.07794227":"0.07079307","0.07797000":"0.00135170","0.07799991":"3.53055283","0.07800000":"15.44243339","0.07810000":"0.00512164","0.07811000":"0.06528559","0.07811050":"1.17651410","0.07812700":"0.30000000","0.07814031":"1.16404526","0.07815343":"0.03063625","0.07818236":"0.00129185","0.07819051":"0.00703410","0.07824132":"0.29585510","0.07825000":"0.00499500","0.07833468":"0.51599889","0.07835000":"0.05000000","0.07836000":"0.25000000","0.07838323":"0.00130542","0.07843631":"0.00229485","0.07844022":"0.06918497","0.07847000":"0.00278655","0.07849581":"0.01454270","0.07850000":"5.10844403","0.07854591":"1.00000000","0.07857424":"2.79977717","0.07861470":"0.00127300","0.07861560":"0.08000000","0.07862000":"0.06486170","0.07862131":"0.01017494","0.07866000":"0.00998000","0.07870000":"0.31440001","0.07877000":"0.05000000","0.07879063":"0.23000000","0.07882110":"0.10000000","0.07882999":"0.00887986","0.07887001":"0.49101180","0.07889000":"0.36744524","0.07889775":"0.01000000","0.07890000":"0.53721715","0.07891012":"0.20000000","0.07898972":"0.07702868","0.07899912":"0.00687175","0.07899998":"29.91879243","0.07900000":"36.29244143","0.07902400":"0.09860000","0.07904591":"1.60930279","0.07907671":"0.00649664","0.07909001":"1.02433522","0.07913000":"0.06444330","0.07920000":"0.69366361","0.07923633":"0.01526984","0.07925000":"0.00499500","0.07935100":"0.06970000","0.07935418":"0.32369337","0.07937000":"0.05000000","0.07938247":"8.69186706","0.07940622":"2.03196600","0.07943000":"0.00380799","0.07948520":"28.09321172","0.07948869":"62.90202256","0.07949000":"0.00140000","0.07949002":"0.00180834","0.07950000":"4.10000000","0.07950252":"1.39594133","0.07955050":"5.00000000","0.07958381":"0.04446130","0.07958999":"0.69839041","0.07960447":"0.47952200","0.07961470":"0.00125700","0.07964000":"0.06403025","0.07970000":"0.77501883","0.07971026":"0.14644839","0.07980000":"0.16486842","0.07986000":"0.05000000","0.07988375":"0.30070397","0.07989200":"0.05873320","0.07990000":"0.47526317","0.07991207":"0.14775225","0.07994999":"0.10000000","0.07997686":"0.01603333","0.07999999":"0.00137500","0.08000000":"89.93769915","0.08000006":"0.10000000","0.08002710":"0.12645553","0.08003036":"1.28847541","0.08009724":"0.75929228","0.08010000":"1.04720000","0.08016000":"0.08953578","0.08021026":"0.06659497","0.08025000":"0.00499500","0.08026380":"6.30000000","0.08031026":"0.07999999","0.08031308":"0.05144507","0.08043977":"2.50000000","0.08044081":"0.00621575","0.08047000":"0.06000000","0.08048000":"0.00139437","0.08049502":"1.26500000","0.08050000":"4.10496895","0.08050252":"1.30000000","0.08054791":"1.72639569","0.08059200":"0.02000000","0.08061470":"0.00124100","0.08064827":"0.12548055","0.08065000":"0.30000000","0.08067000":"0.12643969","0.08071738":"0.01683500","0.08079834":"0.01855315","0.08080000":"1.42576899","0.08085511":"0.01871333","0.08089000":"0.00137223","0.08089999":"0.05000000","0.08094000":"0.12354831","0.08099812":"1.92968584","0.08100000":"20.87398935","0.08100004":"59.88464862","0.08107954":"0.04426645","0.08108000":"0.00604714","0.08109359":"0.25015602","0.08111000":"2.04000000","0.08111823":"0.10000000","0.08111835":"0.05988000","0.08117000":"0.01445995","0.08117939":"0.00383211","0.08117959":"0.12507786","0.08120000":"2.43268053","0.08125000":"0.00499500","0.08126475":"0.01290113","0.08126987":"0.12631440","0.08127020":"0.06313131","0.08129327":"1.20000000","0.08130000":"1.13236602","0.08138855":"0.01211559","0.08139500":"0.31189828","0.08139613":"0.00131005","0.08140000":"1.04640000","0.08145000":"0.02345319","0.08145791":"0.01767675","0.08146300":"0.02532909","0.08150000":"6.00000000","0.08151990":"0.37279239","0.08152000":"10.00000000","0.08152520":"2.00000000","0.08153688":"0.10000000","0.08160000":"1.00000000","0.08161470":"0.00122600","0.08169000":"0.00910138","0.08170000":"1.00000000","0.08173000":"0.04997871","0.08176001":"0.01817555","0.08176450":"0.02000000","0.08180000":"1.01873913","0.08182418":"16.60377538","0.08189775":"0.01000000","0.08190000":"1.13009615","0.08200000":"23.60778894","0.08210000":"1.00487211","0.08217945":"11.71786274","0.08219844":"0.01856058","0.08220000":"1.00000000","0.08220065":"1.00000000","0.08220366":"0.02705370","0.08221500":"1.00000000","0.08222760":"0.02438014","0.08225000":"0.00499500","0.08228200":"20.00000000","0.08230000":"1.00000000","0.08238000":"0.02544135","0.08240000":"1.00000000","0.08248000":"0.00372484","0.08250000":"13.00411317","0.08251396":"2.00000000","0.08252331":"0.01992091","0.08255252":"1.00000000","0.08260000":"0.25246800","0.08261470":"0.00121100","0.08263000":"1.24253732","0.08270000":"1.11606946","0.08275000":"0.04542109","0.08289999":"0.05000000","0.08290000":"0.00482510","0.08293270":"1.08498502","0.08293896":"0.01948861","0.08299000":"0.00248908","0.08300000":"10.86351147","0.08301501":"0.02730828","0.08307521":"0.01789848","0.08310643":"0.00398092","0.08313195":"0.05292718","0.08320000":"0.00261484","0.08323800":"0.03995483","0.08325000":"0.00499500","0.08328203":"0.09079421","0.08330500":"17.21774401","0.08330850":"0.05022784","0.08333217":"3.13076629","0.08333374":"0.05452176","0.08335260":"0.28920000","0.08339251":"0.24515335","0.08344500":"0.02245408","0.08348849":"0.02801770","0.08349189":"0.00119892","0.08350000":"6.00000000","0.08353206":"1.20784373","0.08353401":"0.00263102","0.08357251":"0.24132117","0.08361470":"0.00119700","0.08363500":"0.06323862","0.08364893":"0.07231129","0.08364916":"0.07231109","0.08365000":"6.38228846","0.08367949":"0.02046304","0.08368000":"0.03272097","0.08369880":"0.03617500","0.08370000":"0.00477898","0.08371310":"0.30626650","0.08377000":"0.02435081","0.08377351":"0.12109971","0.08380501":"0.40554961","0.08383711":"0.24219942","0.08384422":"0.00128100","0.08385441":"0.24219942","0.08389775":"0.01000000","0.08390000":"7.38779674","0.08392518":"0.00300977","0.08393000":"0.50000000","0.08393270":"1.30000000","0.08400000":"65.64125268","0.08401798":"0.08372428","0.08403549":"0.04454919","0.08405500":"0.10000000","0.08410000":"0.15742988","0.08414973":"0.03627235","0.08416530":"0.00877109","0.08417524":"0.24130830","0.08419016":"0.10000000","0.08420000":"0.02997000","0.08423005":"0.01343940","0.08423525":"1.43000000","0.08423684":"0.02122875","0.08425000":"0.00499500","0.08428188":"0.02751943","0.08430000":"0.00120334","0.08431000":"0.10000000","0.08431536":"0.10000000","0.08432000":"0.40000000","0.08433290":"0.01606947","0.08434000":"0.06986000","0.08435046":"5.00000000","0.08437294":"0.00777465","0.08439801":"0.19494269","0.08441500":"0.00147395","0.08442002":"0.02148620","0.08443993":"0.33000000","0.08444476":"10.54366917","0.08445300":"4.97404685","0.08449484":"0.40271143","0.08450000":"6.56343094","0.08451150":"0.21660082","0.08458105":"0.35000000","0.08461470":"0.00118200","0.08469000":"0.10000000","0.08470000":"0.87000000","0.08472200":"0.09920400","0.08479000":"0.02406939","0.08485000":"0.06000000","0.08487000":"0.10000000","0.08488200":"58.90530384","0.08490000":"25.26720401","0.08491222":"0.06573392","0.08493120":"0.20664924","0.08495505":"0.25000000","0.08499900":"0.59950000","0.08499999":"0.00129412","0.08500000":"52.75382253","0.08502407":"0.40562267","0.08504000":"0.10950000","0.08507000":"0.12333334","0.08508900":"0.01537269","0.08510000":"0.27081885","0.08511800":"1.19597044","0.08517000":"0.02114367","0.08517343":"0.10000000","0.08520000":"0.17107699","0.08522504":"0.20000000","0.08524000":"0.15642109","0.08525000":"0.00499500","0.08527661":"1.27000000","0.08540000":"0.04430000","0.08540500":"0.07755290","0.08541032":"0.07659501","0.08542747":"0.09790063","0.08544220":"0.00128100","0.08544300":"0.07785728","0.08547600":"1.13985900","0.08550000":"5.01521630","0.08552075":"0.02735204","0.08555000":"0.50000000","0.08560000":"1.00000000","0.08561470":"0.00117000","0.08561584":"0.10437871","0.08562000":"0.01447812","0.08562955":"0.12733137","0.08567196":"0.07873291","0.08570000":"0.09018376","0.08576528":"0.00525976","0.08581500":"0.02854882","0.08587000":"0.00120000","0.08589775":"0.01000000","0.08590000":"1.89315057","0.08596684":"0.00222195","0.08597061":"0.07981308","0.08599000":"2.00000000","0.08599990":"0.31788180","0.08600000":"20.50689318","0.08602309":"0.00255490","0.08607572":"0.01447812","0.08610001":"0.12375000","0.08619502":"0.11184838","0.08638168":"12.08535949","0.08640000":"4.18399210","0.08640500":"0.07059042","0.08642123":"0.00512069","0.08644000":"0.30000000","0.08649993":"0.00128655","0.08650000":"5.32544984","0.08651701":"0.99850000","0.08655556":"0.10000000","0.08659011":"0.19522766","0.08659142":"0.10000000","0.08660000":"16.08081758","0.08660516":"0.04770746","0.08661470":"0.00500000","0.08662000":"0.00577233","0.08662500":"0.02420563","0.08664000":"0.61920761","0.08665500":"0.00139077","0.08666000":"0.36113335","0.08666682":"0.05089499","0.08670000":"0.04360000","0.08674000":"0.05000000","0.08676000":"0.42085520","0.08682000":"0.11862394","0.08684000":"0.12104404","0.08684910":"0.11543000","0.08685589":"0.01464713","0.08685608":"0.48779514","0.08686470":"0.00500000","0.08695001":"2.75206611","0.08697098":"0.25097982","0.08699001":"0.20000000","0.08699993":"0.00128655","0.08700000":"47.66235207","0.08702500":"0.05000000","0.08707650":"0.00291805","0.08711470":"0.00550000","0.08712000":"0.15304561","0.08712100":"0.03989986","0.08713000":"0.03778409","0.08713800":"0.03464060","0.08716712":"0.02406690","0.08717343":"0.10000000","0.08720000":"0.10000000","0.08721221":"0.15721942","0.08721420":"0.01335284","0.08730500":"0.60000000","0.08736470":"0.00300000","0.08737000":"0.00115256","0.08737996":"5.47000000","0.08738416":"1.00000000","0.08738820":"5.00000000","0.08740000":"0.04904457","0.08740500":"0.07612477","0.08743597":"0.00571847","0.08745609":"0.00973048","0.08749993":"0.00125714","0.08750000":"4.80832372","0.08753000":"0.11711618","0.08755556":"0.10000000","0.08761000":"4.46712828","0.08761005":"1.05803905","0.08761470":"0.00550000","0.08770000":"0.67572909","0.08770660":"0.05013584","0.08776000":"0.30000000","0.08777000":"0.00115256","0.08777706":"0.01464713","0.08778999":"15.65015284","0.08783000":"0.00156652","0.08784010":"0.20000000","0.08786470":"0.00500000","0.08787045":"0.43624135","0.08789000":"3.00000000","0.08789008":"0.52194080","0.08789775":"0.01000000","0.08790000":"0.13180200","0.08793981":"0.02385512","0.08797500":"0.05987999","0.08799000":"0.12000000","0.08799236":"2.61272155","0.08799993":"0.00125714","0.08800000":"14.67195763","0.08801000":"0.15000000","0.08802103":"0.11186303","0.08803250":"0.05994000","0.08810000":"0.87048303","0.08811470":"0.00550000","0.08811600":"0.02380816","0.08820001":"0.12080356","0.08829062":"0.00942849","0.08829100":"0.01450071","0.08829815":"0.02995374","0.08830000":"0.85203145","0.08830236":"0.02994000","0.08836470":"0.00300000","0.08843200":"0.10000000","0.08849000":"0.02768144","0.08849993":"0.00125714","0.08850000":"7.82579872","0.08852000":"0.00564844","0.08852661":"0.05987999","0.08853131":"0.00125379","0.08854440":"1.51780855","0.08855556":"0.10000000","0.08858497":"0.02316123","0.08858990":"0.19970002","0.08859520":"0.21000000","0.08861005":"0.05888200","0.08861700":"0.69816268","0.08865904":"0.01120926","0.08867001":"0.15155019","0.08869822":"0.01464713","0.08874000":"0.05000000","0.08876000":"0.30000000","0.08876850":"0.05037561","0.08878000":"3.99600000","0.08878700":"0.00228675","0.08880000":"0.02016785","0.08881374":"1.04779784","0.08882070":"1.00000000","0.08882110":"0.09000000","0.08883000":"0.02000000","0.08885022":"1.61995265","0.08886470":"0.00500000","0.08889000":"9.91354541","0.08889775":"0.01000000","0.08890000":"0.00383656","0.08892408":"0.55847666","0.08896750":"0.02358039","0.08897532":"0.01422661","0.08899000":"0.03527310","0.08899993":"0.00123595","0.08900000":"8.90419928","0.08900961":"0.02993999","0.08902000":"0.06889410","0.08902340":"5.03093809","0.08902500":"0.05000000","0.08906395":"0.02579832","0.08907600":"0.11254400","0.08908800":"0.30000000","0.08910000":"0.00233715","0.08914000":"0.20747631","0.08916668":"0.01170937","0.08917343":"0.10000000","0.08923008":"0.04018486","0.08924191":"0.05987999","0.08926820":"0.99308583","0.08927498":"0.00131960","0.08930666":"0.00225962","0.08932039":"0.04625721","0.08932740":"0.00504756","0.08933021":"0.20306510","0.08935705":"0.00124209","0.08935889":"0.01500000","0.08936470":"0.00500000","0.08940000":"0.04230000","0.08942000":"0.34608083","0.08943500":"0.00802086","0.08944561":"0.00135298","0.08949993":"0.00123595","0.08950000":"36.96865892","0.08950164":"0.02122875","0.08951492":"0.41098961","0.08952000":"10.58583440","0.08953581":"0.02388901","0.08955556":"0.10000000","0.08956963":"0.02349820","0.08957699":"0.00954804","0.08960767":"0.20000000","0.08961470":"0.00550000","0.08964000":"0.25549074","0.08968388":"0.01115027","0.08969193":"0.00391969","0.08970000":"0.00237915","0.08974744":"0.08000000","0.08977000":"0.10000000","0.08977488":"55.69486078","0.08978500":"0.25000000","0.08982217":"0.04599880","0.08983003":"23.55719676","0.08985000":"0.06172037","0.08986470":"0.00500000","0.08986600":"1.26625124","0.08987000":"5.00000000","0.08988123":"0.40929423","0.08988900":"0.00200000","0.08990000":"0.59925000","0.08995000":"0.01222556","0.08995400":"0.01252745","0.08995808":"0.59757002","0.08996489":"1.00000000","0.08996830":"3.50995182","0.08998507":"0.00920714","0.08999900":"0.05493999","0.08999993":"0.00123595","0.08999999":"0.00122222","0.09000000":"382.27545243","0.09000001":"0.10000000","0.09000400":"0.02000000","0.09000447":"0.10000000","0.09002171":"0.06982500","0.09003700":"0.05000000","0.09003925":"0.05987999","0.09006307":"0.16915274","0.09007000":"0.00169825","0.09007400":"0.05000000","0.09008486":"0.02993999","0.09011100":"2.81267620","0.09011470":"0.00550000","0.09011500":"5.00000000","0.09012895":"0.02994000","0.09013000":"0.04530085","0.09014239":"0.01748428","0.09015706":"0.00661426","0.09019876":"0.70319186","0.09021750":"0.02993999","0.09030000":"0.14085679","0.09030554":"0.00160473","0.09031574":"0.57127847","0.09033325":"0.50000000","0.09036470":"0.00400000","0.09040000":"3.98785655","0.09041007":"0.00122773","0.09049993":"0.00123595","0.09050000":"2.74627034","0.09056825":"0.08981998","0.09057365":"1.56560207","0.09061470":"0.00550000","0.09061723":"0.01499382","0.09061811":"0.00954804","0.09062811":"0.01733546","0.09070000":"0.04170000","0.09071261":"0.00200000","0.09072451":"0.61607264","0.09073000":"0.00551085","0.09078000":"0.11042800","0.09085586":"0.03091344","0.09086470":"0.00500000","0.09088000":"0.30000000","0.09088796":"0.00354325","0.09090000":"0.27502750","0.09090889":"5.26925805","0.09090900":"0.53352200","0.09099208":"0.63939993","0.09099900":"1.18441536","0.09099939":"0.00110990","0.09099993":"0.00120879","0.09100000":"5.55443088","0.09100447":"0.10000000","0.09100501":"0.00200000","0.09100503":"49.80772000","0.09102409":"0.04539142","0.09106230":"0.11410432","0.09109486":"0.04030000","0.09109500":"0.00300000","0.09109605":"5.09189238","0.09111000":"0.50000000","0.09111470":"0.00550000","0.09111550":"0.77118728","0.09130000":"1.02000000","0.09138000":"0.04377325","0.09139687":"0.42224565","0.09144009":"3.85350453","0.09148000":"0.38941500","0.09149993":"0.00120879","0.09150000":"0.38735064","0.09150255":"0.00880431","0.09152000":"10.00000000","0.09159530":"0.00338445","0.09161470":"0.00500000","0.09161665":"0.18392617","0.09162000":"0.08187152","0.09165922":"0.00954804","0.09180000":"2.00099700","0.09188198":"0.03896831","0.09189437":"0.01859764","0.09189775":"0.01000000","0.09198000":"0.10871929","0.09199900":"0.36868326","0.09199993":"0.00119565","0.09200000":"6.61045754","0.09202608":"0.68415792","0.09208000":"0.30000000","0.09210000":"0.12000000","0.09211470":"0.00500000","0.09211581":"0.00950602","0.09217343":"0.10000000","0.09218600":"0.01064483","0.09220065":"1.00000000","0.09228200":"30.00000000","0.09230000":"0.21668472","0.09233914":"0.00389449","0.09234063":"0.09251583","0.09240000":"0.11531250","0.09243117":"2.99550000","0.09245005":"0.24842726","0.09248459":"5.55674745","0.09249993":"0.00119565","0.09250000":"0.12472032","0.09250156":"0.06223776","0.09261470":"0.00500000","0.09264999":"1.29232259","0.09265181":"0.05000000","0.09266447":"0.10000000","0.09270033":"0.00954804","0.09270901":"20.11314509","0.09273638":"0.02064558","0.09274949":"6.54939660","0.09276001":"0.27727393","0.09290500":"0.00400000","0.09298000":"0.01116893","0.09299898":"0.01500000","0.09299993":"0.00129032","0.09300000":"3.58608852","0.09300054":"0.07523157","0.09300447":"0.10000000","0.09305000":"1.00000000","0.09306008":"0.73204897","0.09309501":"0.11721477","0.09311470":"0.00500000","0.09312000":"0.10738832","0.09312022":"0.00118126","0.09315001":"0.01000092","0.09316138":"0.15000000","0.09325258":"0.09947540","0.09332200":"0.02162280","0.09340000":"0.04050000","0.09340100":"0.40000000","0.09341422":"0.14265513","0.09348657":"0.32357689","0.09348658":"0.32357678","0.09348661":"0.32378617","0.09348906":"0.32388391","0.09348908":"0.32388377","0.09349415":"0.32352410","0.09349993":"0.00129032","0.09350000":"0.00108791","0.09350254":"0.96250020","0.09355210":"0.02476402","0.09361470":"0.00500000","0.09363781":"0.01067945","0.09370000":"30.00000000","0.09373500":"0.03966349","0.09373630":"0.00200000","0.09374000":"2.34832356","0.09374145":"0.00954804","0.09387384":"0.14720924","0.09388500":"1.24000000","0.09389028":"7.24024222","0.09389476":"0.00475204","0.09389775":"0.01000000","0.09399993":"0.00129032","0.09400000":"3.79289619","0.09400447":"0.10000000","0.09409408":"0.78329240","0.09411470":"0.00500000","0.09412592":"1.09356493","0.09420000":"0.70000000","0.09420953":"0.00433349","0.09421492":"0.00158430","0.09423027":"0.30000000","0.09423054":"0.83624604","0.09426494":"0.26013238","0.09431000":"0.10000000","0.09438000":"0.03491259","0.09439926":"0.00704042","0.09439952":"0.03792907","0.09440000":"0.05000000","0.09443113":"0.00529486","0.09443894":"0.01866981","0.09443993":"0.34000000","0.09445056":"0.01091250","0.09449484":"0.40000003","0.09449993":"0.00126984","0.09450000":"0.32797215","0.09452000":"5.00000000","0.09456466":"0.00131238","0.09457831":"0.00567973","0.09460047":"0.00124789","0.09461470":"0.00500000","0.09463483":"0.00269131","0.09468977":"52.80400905","0.09470000":"0.83790000","0.09473000":"0.00105597","0.09475000":"0.40000000","0.09477613":"0.08013514","0.09480000":"13.97900001","0.09480614":"0.03776639","0.09488972":"0.02588209","0.09490000":"0.25000000","0.09491058":"0.69895000","0.09492523":"0.10000000","0.09499900":"0.44099688","0.09499931":"0.13083128","0.09499999":"0.50271382","0.09500000":"21.08020907","0.09503000":"0.50000000","0.09505000":"1.00000000","0.09505362":"0.01142348","0.09508000":"0.30000000","0.09510006":"0.00127070","0.09511470":"0.00500000","0.09512809":"0.83812287","0.09517900":"0.00966155","0.09520000":"0.00106019","0.09523554":"0.04030000","0.09538410":"0.32198878","0.09540000":"0.09611231","0.09547750":"0.00451907","0.09549976":"1.75614696","0.09553587":"0.44978967","0.09560000":"2.50000000","0.09561470":"0.00500000","0.09562811":"0.01103411","0.09566447":"0.10000000","0.09566521":"0.02051431","0.09570000":"0.05123103","0.09572000":"0.31714201","0.09580000":"0.12222679","0.09583819":"0.00399400","0.09584168":"0.03424137","0.09589775":"0.01000000","0.09590000":"0.05170599","0.09593605":"0.10848816","0.09598820":"5.00000000","0.09600000":"2.80179942","0.09600020":"0.13971261","0.09600447":"0.10000000","0.09603033":"0.03938266","0.09604365":"0.00526046","0.09608000":"0.30000000","0.09609500":"0.00437375","0.09610480":"0.03000000","0.09610952":"0.00269931","0.09611470":"0.00500000","0.09614005":"0.01649717","0.09616209":"0.89679147","0.09619439":"0.02368361","0.09620000":"0.06513804","0.09620468":"1.74470666","0.09622000":"0.50000000","0.09622081":"0.01505890","0.09624604":"0.10299257","0.09628750":"0.15590360","0.09640000":"2.31660936","0.09650000":"4.14063490","0.09651569":"0.02588209","0.09654952":"0.03708445","0.09655039":"0.09054001","0.09660000":"0.11029891","0.09660063":"2.11980253","0.09661200":"4.80134611","0.09661470":"0.00500000","0.09666060":"0.00917166","0.09677500":"0.02123445","0.09685000":"0.00104249","0.09690000":"0.01045805","0.09700000":"25.11786823","0.09703690":"0.03260485","0.09705000":"1.00000000","0.09708000":"0.30000000","0.09709800":"0.00109662","0.09711470":"0.00600000","0.09712022":"0.00118126","0.09712407":"0.00114286","0.09712500":"0.02795800","0.09718554":"0.20000000","0.09719609":"0.95956688","0.09721000":"0.46779899","0.09736000":"10.00000000","0.09740000":"0.03880000","0.09741579":"0.08282078","0.09742000":"0.50000000","0.09746079":"0.08282078","0.09746099":"0.08282078","0.09746669":"0.08282078","0.09750000":"0.05434585","0.09750502":"0.15979501","0.09755148":"0.08363346","0.09760000":"4.44106282","0.09761470":"0.00600000","0.09762269":"0.08282078","0.09766447":"0.10000000","0.09768394":"0.10457170","0.09768759":"2.00000000","0.09770000":"0.00228675","0.09770680":"0.01023470","0.09773303":"0.11970000","0.09777000":"0.03305546","0.09779000":"2.05876289","0.09789000":"2.99999999","0.09789775":"0.01000000","0.09790000":"0.00698189","0.09790900":"0.26808480","0.09792012":"0.00322432","0.09796000":"0.11695242","0.09799150":"0.11975999","0.09800000":"2.35211445","0.09801581":"0.00779497","0.09802596":"2.06584578","0.09804362":"0.51440329","0.09810000":"1.00000000","0.09811470":"0.00600000","0.09812760":"5.26323309","0.09814165":"0.02588209","0.09814174":"0.16600847","0.09815000":"10.82605068","0.09815500":"0.01327779","0.09820000":"0.84857500","0.09822165":"0.02265595","0.09822221":"0.23000000","0.09823009":"1.02673656","0.09824761":"0.00112519","0.09825140":"0.00112516","0.09825600":"0.01283803","0.09830000":"0.14964252","0.09834000":"0.30000000","0.09840000":"0.61281654","0.09846105":"0.02751118","0.09850000":"2.23518991","0.09860000":"1.22620762","0.09860320":"0.03013339","0.09861362":"0.00127644","0.09861470":"0.00600000","0.09861573":"0.02413414","0.09864000":"0.50000000","0.09866284":"0.03206530","0.09870000":"1.06187765","0.09870001":"0.10795212","0.09870940":"0.08501134","0.09880000":"0.49896131","0.09880479":"0.17399068","0.09881230":"0.41288277","0.09881820":"0.03623306","0.09882666":"50.59363025","0.09887696":"0.00642750","0.09887810":"0.11970000","0.09889775":"0.01000000","0.09890000":"4.15813014","0.09893861":"0.02625753","0.09895167":"0.01353968","0.09899801":"0.00663771","0.09899839":"0.10782637","0.09899885":"0.63072615","0.09900000":"133.73705206","0.09900800":"0.00121202","0.09903506":"0.31921733","0.09905000":"0.11205892","0.09907210":"0.46779899","0.09910000":"0.01957105","0.09911111":"1.00000000","0.09911206":"0.14977500","0.09911470":"0.00550000","0.09911526":"0.01314653","0.09917422":"0.00111802","0.09917497":"0.02123174","0.09920000":"0.10667028","0.09922110":"26.70731018","0.09922112":"0.20212419","0.09926409":"1.09860812","0.09927651":"0.00866600","0.09929098":"0.29817457","0.09929400":"0.50355500","0.09932016":"2.00749002","0.09933034":"0.03604625","0.09934000":"0.05000000","0.09934054":"0.23489664","0.09937147":"0.50636744","0.09937621":"0.04045589","0.09940000":"1.06986598","0.09940500":"3.28691052","0.09944500":"0.41503918","0.09945969":"0.17757552","0.09949000":"0.02010252","0.09949503":"0.02204037","0.09949635":"0.25738653","0.09950000":"0.07844223","0.09952000":"5.00000000","0.09956525":"0.02186539","0.09958544":"0.00880154","0.09960029":"0.04091354","0.09961470":"0.00500000","0.09970000":"1.07343980","0.09970151":"0.00261341","0.09970170":"2.05156157","0.09972401":"0.10074505","0.09972554":"0.02743067","0.09976762":"0.02588209","0.09976999":"0.01279050","0.09978866":"0.03990000","0.09980000":"0.10000000","0.09980667":"0.00154942","0.09982000":"0.20430302","0.09982802":"0.00180310","0.09987000":"5.50000000","0.09988940":"0.00175288","0.09989029":"0.04016143","0.09990000":"9.49555279","0.09990503":"3.30990527","0.09992595":"0.01574225","0.09994000":"0.02726569","0.09994010":"0.52000000","0.09995000":"0.00200000","0.09998243":"0.71958263","0.09999000":"15.03387533","0.09999100":"0.25648085","0.09999900":"0.06275390","0.09999919":"0.07595347","0.09999998":"0.21945000","0.09999999":"0.00116678","0.10000000":"320.83090700","0.10000001":"0.02830020","0.10000002":"0.02537483","0.10000003":"0.02496250","0.10000004":"0.02435365","0.10000005":"0.02435365","0.10000006":"0.02349412","0.10000007":"0.03524117","0.10000008":"0.02295402","0.10000009":"0.02279555","0.10000010":"0.02258287","0.10000011":"0.02233267","0.10000447":"0.10000000","0.10001230":"0.01759025","0.10010000":"0.00100911","0.10012301":"0.19970568","0.10013000":"0.00248320","0.10015536":"0.20000000","0.10016070":"0.02204037","0.10018569":"0.00997500","0.10019355":"0.01765962","0.10020000":"0.19970000","0.10020114":"0.04050863","0.10022000":"0.04161652","0.10029119":"0.04034685","0.10029809":"1.17551068","0.10030795":"0.01035253","0.10031500":"0.20171732","0.10031760":"0.00207221","0.10036298":"0.09975000","0.10040000":"1.01062756","0.10043944":"0.04000000","0.10048979":"0.50000000","0.10050000":"0.60300717","0.10052472":"0.05003689","0.10054746":"0.02203312","0.10059898":"0.10300000","0.10060000":"0.01878157","0.10061129":"0.03983603","0.10064334":"0.01426027","0.10069000":"0.00106356","0.10069234":"0.01710513","0.10071107":"0.09984999","0.10075000":"0.99710406","0.10079000":"0.01000000","0.10080000":"0.10570312","0.10080467":"0.00992695","0.10082604":"0.05000000","0.10082636":"0.02204037","0.10083900":"0.04165726","0.10089000":"0.00499250","0.10096280":"0.35992779","0.10099950":"0.00200000","0.10100000":"9.19589910","0.10100158":"0.17443750","0.10105917":"0.00455339","0.10106761":"0.03820843","0.10107316":"0.01904951","0.10109900":"0.02000000","0.10112494":"0.03540656","0.10119999":"0.70000000","0.10123027":"0.07730310","0.10124081":"0.10123098","0.10128152":"1.99289572","0.10130000":"0.00110492","0.10133209":"1.25779643","0.10139000":"0.19725811","0.10139358":"0.02588209","0.10140000":"0.05025263","0.10140396":"0.06224707","0.10144413":"0.00492882","0.10144495":"0.00997500","0.10145000":"0.09786012","0.10149202":"0.02204037","0.10149343":"1.00031439","0.10150000":"0.04896268","0.10151549":"0.05089705","0.10152000":"9.06683745","0.10153046":"0.01095316","0.10161408":"0.00991472","0.10163713":"0.37517557","0.10164750":"0.03658725","0.10165192":"0.03522300","0.10168377":"0.04181513","0.10170000":"0.01000000","0.10173903":"0.04004249","0.10174422":"0.33382070","0.10175730":"0.10028078","0.10177659":"0.10024920","0.10178584":"0.11024628","0.10179478":"0.01635548","0.10180896":"0.06171157","0.10187080":"0.05223548","0.10189203":"0.00981431","0.10189707":"0.00498500","0.10190000":"0.10991262","0.10190034":"0.00590640","0.10198822":"0.09569270","0.10199990":"0.39835001","0.10200000":"0.14302513","0.10200045":"0.02766856","0.10210392":"0.10000000","0.10211162":"0.12032117","0.10211376":"0.00100091","0.10212022":"0.00098021","0.10212178":"0.01098285","0.10215769":"0.02204037","0.10220491":"0.03220728","0.10225200":"0.01996000","0.10225485":"0.01000000","0.10230100":"0.01000000","0.10232633":"0.02345691","0.10236609":"1.34584218","0.10238329":"0.03497139","0.10250000":"0.99504782","0.10253391":"0.03492002","0.10257996":"0.74380955","0.10260000":"0.01000000","0.10262043":"0.00107726","0.10263000":"0.03423808","0.10267000":"0.51568540","0.10268580":"0.00902416","0.10268759":"2.00000000","0.10269000":"0.01065736","0.10270000":"0.03680000","0.10274578":"0.00468038","0.10280000":"0.08853008","0.10280719":"0.00156172","0.10281780":"0.52000000","0.10281945":"0.00134585","0.10282335":"0.02204037","0.10290000":"0.10355400","0.10297127":"0.01657826","0.10299107":"0.03118527","0.10299323":"0.00516876","0.10300000":"15.91965753","0.10308670":"0.00195856","0.10309982":"0.00105713","0.10310364":"2.57044588","0.10312971":"0.01087044","0.10318055":"0.72397540","0.10320000":"0.12700000","0.10321000":"0.53220101","0.10325990":"0.00181969","0.10327784":"0.03466848","0.10332001":"0.01304950","0.10332572":"0.48811788","0.10340009":"1.44005113","0.10347390":"0.99750000","0.10350000":"0.14837046","0.10352436":"5.00000000","0.10354511":"0.03953902","0.10360068":"0.02031575","0.10361000":"0.01000000","0.10366068":"0.03454045","0.10366650":"0.03645043","0.10372977":"0.00127298","0.10376237":"0.40031466","0.10379958":"0.01826274","0.10397997":"1.02835983","0.10400000":"0.81951377","0.10400722":"1.00000000","0.10408193":"0.02345691","0.10409900":"0.02500000","0.10410000":"0.15000000","0.10419594":"0.03436301","0.10423192":"0.43661970","0.10426271":"0.02095486","0.10431000":"0.80000000","0.10434990":"0.04000000","0.10438000":"0.01000000","0.10443409":"1.54085471","0.10446478":"0.09572604","0.10449018":"0.01713341","0.10449499":"0.04992500","0.10451500":"0.02457619","0.10452000":"0.01597209","0.10453250":"0.95664000","0.10459860":"0.00453797","0.10468000":"0.12005853","0.10470000":"0.25900000","0.10480000":"0.00731078","0.10488098":"47.67308166","0.10490000":"1.00000000","0.10491902":"0.07929117","0.10499400":"1.25340238","0.10499999":"0.00096190","0.10500000":"4.57490452","0.10500001":"0.10046025","0.10508761":"0.55490000","0.10509802":"8.85279555","0.10511500":"5.00000000","0.10513335":"0.00373191","0.10513637":"0.03760000","0.10520000":"0.21130780","0.10520499":"0.01826274","0.10523057":"0.03248328","0.10531572":"0.29955000","0.10540000":"0.04590000","0.10541145":"0.00434553","0.10545000":"0.02421121","0.10546809":"1.64871454","0.10548261":"0.00795771","0.10550000":"1.00120000","0.10552000":"0.50000000","0.10560049":"0.00111396","0.10562110":"0.39171928","0.10565017":"0.20389496","0.10570000":"2.04133529","0.10575230":"0.10534789","0.10583753":"0.02345691","0.10584839":"0.05132190","0.10595367":"0.47526110","0.10600000":"0.30564729","0.10604344":"0.19360979","0.10611591":"0.47482890","0.10613012":"0.51187656","0.10619467":"0.00941667","0.10620000":"0.49890004","0.10625000":"0.03266595","0.10625821":"0.03369609","0.10627500":"0.00690566","0.10628900":"0.01416839","0.10635088":"0.19223907","0.10640287":"0.33547844","0.10640650":"31.62602994","0.10641500":"0.24938728","0.10650000":"0.10661620","0.10650210":"1.76412456","0.10656291":"0.00094780","0.10659000":"0.03091332","0.10661040":"0.01826274","0.10663579":"0.00111396","0.10670000":"0.03540000","0.10689000":"1.00000000","0.10689895":"0.08916131","0.10690000":"0.00447724","0.10696280":"0.47145677","0.10698000":"0.03994000","0.10700000":"0.63318514","0.10702440":"0.01600000","0.10704000":"0.00998500","0.10707870":"0.50000000","0.10711093":"0.05016825","0.10713600":"0.41800921","0.10716021":"1.10134552","0.10717911":"0.00970191","0.10728431":"0.09659182","0.10730714":"0.00100713","0.10731100":"0.01676620","0.10733106":"0.03199672","0.10737944":"0.03334425","0.10738600":"0.09985000","0.10739105":"0.04992501","0.10739118":"5.00000000","0.10740000":"0.08912750","0.10741970":"0.60000000","0.10748940":"0.00100000","0.10750000":"28.06502832","0.10751000":"0.10007413","0.10751500":"0.10253647","0.10752000":"0.18601190","0.10753610":"1.88761328","0.10757564":"0.00474363","0.10759313":"0.02345691","0.10765769":"0.03325806","0.10767109":"0.00111396","0.10768000":"0.16538522","0.10768759":"2.00000000","0.10770438":"0.09417307","0.10774400":"0.00216606","0.10778469":"0.01702336","0.10780000":"0.30953500","0.10788767":"0.09975000","0.10792000":"0.12354831","0.10796166":"0.51432449","0.10800000":"1.93062667","0.10801581":"0.01826274","0.10806882":"0.01830807","0.10810523":"5.02789128","0.10815544":"0.02356121","0.10822606":"0.25203733","0.10827600":"0.05095410","0.10830000":"0.29606187","0.10837841":"0.00101496","0.10840000":"0.01000000","0.10850000":"0.06928875","0.10851985":"0.00110578","0.10882070":"1.00000000","0.10889100":"0.00092000","0.10892426":"0.01882170","0.10897000":"0.10000000","0.10900000":"3.96793986","0.10903027":"0.02012657","0.10910000":"0.01000000","0.10912022":"0.00091733","0.10925000":"0.15648626","0.10926240":"0.03276961","0.10926408":"0.01733804","0.10934873":"0.02345691","0.10940000":"0.03460000","0.10942122":"0.01826274","0.10948531":"0.46187377","0.10950000":"0.09348641","0.10955590":"0.01995000","0.10957581":"1.72034920","0.10959523":"0.01842591","0.10961701":"0.06686239","0.10971265":"0.00105713","0.10977358":"2.49625005","0.10980000":"4.08117022","0.10984880":"45.51710665","0.10990000":"0.40000000","0.10995010":"0.58494586","0.10996574":"0.99920001","0.10998000":"0.83838352","0.10998510":"0.00099831","0.10999888":"0.19916223","0.10999900":"0.02500000","0.10999998":"0.05075284","0.10999999":"0.09699609","0.11000000":"49.57312437","0.11002883":"0.60010659","0.11008449":"0.04129158","0.11010000":"0.05000000","0.11017001":"0.10095342","0.11020000":"1.00000000","0.11020054":"0.10021238","0.11025000":"0.09664285","0.11025018":"0.07340226","0.11027000":"0.11084604","0.11028000":"0.18135655","0.11028951":"1.29838133","0.11028963":"0.79999854","0.11030000":"0.00508435","0.11050000":"0.02877449","0.11050713":"0.00418938","0.11052139":"1.99999725","0.11057000":"0.50000000","0.11059210":"0.03883000","0.11060000":"0.10493860","0.11061586":"0.00904030","0.11070000":"0.29932150","0.11071001":"0.03204255","0.11074348":"0.01733804","0.11088000":"1.11734891","0.11100000":"6.18790656","0.11100902":"0.10000000","0.11104541":"0.00509647","0.11111111":"0.05822666","0.11111514":"0.09985000","0.11112390":"0.10668606","0.11119094":"0.60085937","0.11130000":"0.31272727","0.11130210":"0.11585065","0.11131897":"0.77727408","0.11133304":"5.75525602","0.11135000":"0.14955000","0.11140000":"0.74990848","0.11141050":"0.20459000","0.11145005":"0.20428029","0.11151396":"1.40000000","0.11159641":"0.03208424","0.11179999":"0.01088440","0.11187661":"0.10594383","0.11200000":"0.65780331","0.11203811":"1.34697781","0.11210000":"1.05105264","0.11210392":"0.10000000","0.11220000":"0.20000000","0.11222075":"0.00185238","0.11222288":"0.01733804","0.11230000":"1.00000000","0.11231000":"6.00000000","0.11235822":"0.79534079","0.11237547":"0.00719496","0.11240000":"0.03000000","0.11245624":"0.04992501","0.11250000":"0.00100000","0.11250100":"0.48144923","0.11261434":"0.01067673","0.11267478":"0.55712288","0.11270000":"0.20000000","0.11274000":"0.10015559","0.11279000":"0.02873772","0.11289069":"0.50000000","0.11294889":"0.11982000","0.11300000":"2.24974192","0.11301600":"0.49116537","0.11310000":"1.50000000","0.11311000":"0.01839812","0.11316138":"0.15000000","0.11320729":"0.01444029","0.11326066":"0.02460342","0.11328357":"0.01506891","0.11336400":"0.02323075","0.11340000":"0.03330000","0.11350000":"0.00798600","0.11356190":"0.14544720","0.11356891":"0.00090000","0.11357465":"0.01500000","0.11358236":"0.51013384","0.11362000":"0.20717075","0.11362309":"0.01118031","0.11370228":"0.01733804","0.11379183":"0.03146523","0.11399050":"0.01779989","0.11400000":"2.24702314","0.11403500":"0.01588581","0.11412022":"0.00091733","0.11423789":"0.05000000","0.11438748":"0.01426429","0.11439000":"0.00174917","0.11464009":"0.00598200","0.11470000":"0.29300000","0.11477230":"0.00997500","0.11477613":"0.06462725","0.11480984":"43.55027030","0.11481242":"0.41566703","0.11486000":"0.16670483","0.11490000":"0.25000000","0.11499999":"0.00087826","0.11500000":"12.26920688","0.11502540":"0.73374982","0.11515670":"0.00868382","0.11518168":"0.01733804","0.11519490":"0.00099700","0.11521043":"0.02795149","0.11536045":"0.00398800","0.11537716":"0.03103288","0.11541359":"0.00104804","0.11550000":"0.09260000","0.11556351":"0.36239825","0.11564500":"0.09664172","0.11568910":"0.00086855","0.11571000":"0.10010162","0.11574751":"0.00593158","0.11575575":"0.00099700","0.11585027":"0.01153167","0.11593168":"0.00199400","0.11599119":"1.00000000","0.11600000":"9.25040975","0.11604541":"0.00509647","0.11612000":"0.50000000","0.11616890":"0.02787570","0.11624393":"0.02323410","0.11624600":"0.00090000","0.11643123":"0.25000000","0.11650544":"0.00199400","0.11660000":"2.00000000","0.11684502":"0.08575635","0.11700000":"0.58504272","0.11740000":"0.03220000","0.11745000":"0.23274012","0.11746000":"0.00090000","0.11750000":"0.00100000","0.11754686":"0.00952171","0.11756000":"0.03226394","0.11756460":"0.00185238","0.11759000":"0.02995500","0.11768001":"1.01039396","0.11768735":"0.04512318","0.11771000":"0.01766484","0.11777000":"50.00000000","0.11800000":"36.81165122","0.11800652":"0.01000000","0.11802088":"0.00876955","0.11812405":"0.01895123","0.11817538":"0.00625363","0.11820000":"20.00000000","0.11821200":"0.03368939","0.11824133":"0.00099700","0.11824600":"0.00090000","0.11826818":"0.03027430","0.11828100":"0.01240843","0.11846065":"0.08451915","0.11849000":"0.10020521","0.11860000":"0.01265815","0.11864500":"1.10261394","0.11866805":"2.06189621","0.11870000":"0.03180000","0.11870570":"0.13380640","0.11882070":"1.00000000","0.11886711":"0.03012176","0.11888000":"0.50000000","0.11888918":"0.08513440","0.11892516":"0.00267336","0.11896287":"0.02000000","0.11900000":"12.31921007","0.11906609":"0.00095966","0.11909025":"0.10972500","0.11910000":"0.10137323","0.11910267":"0.06433841","0.11914632":"0.10554816","0.11926997":"0.00093066","0.11927016":"0.00084682","0.11932101":"0.01243697","0.11932418":"0.01841691","0.11933900":"0.05000000","0.11939572":"0.11937471","0.11945800":"0.06000000","0.11946268":"0.00876955","0.11948590":"0.05000000","0.11949986":"6.52518544","0.11950395":"38.85929407","0.11954720":"0.05866806","0.11968000":"0.16711229","0.11970000":"0.20000000","0.11977204":"0.01455774","0.11980000":"0.78350341","0.11981480":"41.73106814","0.11981827":"0.00834597","0.11982604":"0.05000000","0.11989800":"0.08246482","0.11990000":"2.63794453","0.11990807":"0.17039627","0.11999900":"0.02541250","0.11999990":"0.10158984","0.11999999":"0.00084167","0.12000000":"155.24152222","0.12010150":"1.38291291","0.12024667":"0.00720480","0.12031880":"1.00000000","0.12044844":"0.38100000","0.12046000":"0.00084000","0.12046159":"0.11219812","0.12054351":"0.59548746","0.12059500":"1.00000000","0.12060000":"3.87460710","0.12061009":"0.29816861","0.12061018":"0.29816841","0.12061293":"0.30346486","0.12067483":"0.33576987","0.12070000":"5.09422585","0.12073000":"0.10014279","0.12073400":"1.00000000","0.12080000":"0.53664290","0.12081003":"0.99750001","0.12086000":"0.00338650","0.12092997":"0.83373092","0.12094940":"0.64351324","0.12096771":"0.05491751","0.12100000":"11.97492645","0.12106000":"0.11731636","0.12106533":"0.02739785","0.12110000":"0.05000000","0.12110715":"0.00144637","0.12120000":"0.14202166","0.12121974":"0.00148490","0.12122000":"3.00000000","0.12122885":"0.15022838","0.12140000":"0.33110000","0.12141500":"0.01885285","0.12143328":"0.02063000","0.12146000":"0.00090000","0.12149994":"0.08608877","0.12150000":"0.01194063","0.12164518":"0.01646805","0.12168407":"0.00413027","0.12173300":"0.04800000","0.12180000":"0.17291653","0.12186414":"3.81535230","0.12190000":"0.19966143","0.12193855":"2.06633219","0.12196476":"0.29864454","0.12200000":"2.77564245","0.12207085":"0.01180667","0.12210392":"0.05961000","0.12212022":"0.00081968","0.12213000":"0.20000000","0.12213394":"0.02093681","0.12215000":"0.00090000","0.12218459":"0.12140861","0.12220000":"0.01100000","0.12221350":"1.28410037","0.12225000":"0.00090000","0.12227299":"0.00375690","0.12230000":"0.00844567","0.12235000":"0.00090000","0.12238000":"0.05270798","0.12240000":"0.02033150","0.12240001":"0.29613095","0.12245000":"0.00090000","0.12249499":"0.07186515","0.12249997":"0.00090000","0.12250000":"0.30321050","0.12251850":"0.02504918","0.12252000":"0.14982386","0.12255006":"0.00703770","0.12259800":"0.10000000","0.12264000":"0.10871929","0.12265000":"0.00090000","0.12269997":"0.00090000","0.12270000":"0.03080000","0.12275000":"0.00090000","0.12277300":"0.00821469","0.12279997":"0.00090000","0.12280000":"1.00374103","0.12282440":"0.04246159","0.12285000":"0.00090000","0.12290000":"0.20000000","0.12292426":"0.01836142","0.12295000":"0.00090000","0.12300000":"4.05263859","0.12308486":"0.02379851","0.12310000":"0.04500000","0.12311000":"0.49925000","0.12318444":"0.02379851","0.12328326":"0.02489379","0.12330000":"4.55776802","0.12333000":"0.19489685","0.12338416":"0.85000000","0.12338810":"0.04100000","0.12343152":"0.02131954","0.12345000":"0.01000000","0.12346516":"0.00723830","0.12349000":"0.46632854","0.12350000":"0.51943595","0.12354683":"0.11741361","0.12360000":"0.04992500","0.12369851":"0.69895001","0.12373346":"0.14116018","0.12382010":"1.00000000","0.12386330":"0.10501466","0.12390000":"0.40000000","0.12395798":"0.03511308","0.12400000":"1.33336958","0.12402113":"0.29146577","0.12416000":"0.10738832","0.12420000":"0.10000000","0.12420007":"0.01190813","0.12428502":"0.45757791","0.12429799":"0.02313076","0.12430000":"0.10759215","0.12431619":"0.01168640","0.12432019":"0.16000000","0.12435216":"0.29146577","0.12440000":"0.00196396","0.12444210":"0.10000000","0.12449511":"10.00000000","0.12449999":"0.05000000","0.12450000":"2.99400000","0.12455099":"0.01129187","0.12460000":"0.20300000","0.12460001":"0.16466997","0.12460162":"0.00802558","0.12463529":"2.49537375","0.12480000":"0.80625569","0.12488685":"1.10759318","0.12489814":"40.03261858","0.12497648":"2.00000000","0.12499000":"0.50000000","0.12500000":"31.34955208","0.12502079":"0.02379851","0.12502540":"0.80000000","0.12510010":"0.16520742","0.12520000":"0.50000000","0.12527500":"5.98650000","0.12530000":"0.03020000","0.12535000":"0.01995001","0.12535544":"1.15744083","0.12540000":"0.03111801","0.12550000":"0.23940000","0.12580200":"0.00720497","0.12583000":"0.02125868","0.12591560":"0.07691154","0.12592576":"0.01028553","0.12596000":"0.64281138","0.12599999":"0.00080159","0.12600000":"9.02951350","0.12604475":"0.49585565","0.12606024":"0.21195370","0.12613045":"0.01496250","0.12620000":"0.10000000","0.12645110":"0.04000000","0.12645801":"0.02379851","0.12648893":"0.01509000","0.12650000":"0.13608533","0.12650845":"2.80000000","0.12651992":"0.00279717","0.12654680":"1.32705899","0.12660000":"0.07500000","0.12661501":"0.00335812","0.12664957":"10.80702897","0.12665765":"0.00193058","0.12666509":"1.74022661","0.12667000":"0.29746585","0.12668003":"2.20473407","0.12670000":"0.02980000","0.12676500":"0.01201303","0.12680101":"0.02655854","0.12689000":"1.00000000","0.12695792":"0.29146577","0.12697870":"2.00000000","0.12700000":"0.35838398","0.12700519":"0.17235269","0.12708465":"0.01995000","0.12710000":"0.01856805","0.12713802":"0.67085908","0.12720000":"0.01855345","0.12726655":"0.25000000","0.12730000":"0.01853888","0.12734001":"0.54146029","0.12737498":"0.04000000","0.12740000":"0.01852433","0.12749499":"0.02000000","0.12750000":"1.11932058","0.12758697":"0.00401092","0.12760000":"0.01849529","0.12761900":"0.50000000","0.12764800":"2.00000000","0.12770000":"0.01848081","0.12780000":"0.01846635","0.12783086":"0.00375690","0.12786000":"0.15642109","0.12790000":"0.01845191","0.12791676":"1.32914154","0.12799513":"0.14574713","0.12800000":"40.47431583","0.12800512":"2.35255311","0.12810000":"0.26842310","0.12820000":"25.11840873","0.12829127":"0.21907761","0.12831190":"0.02942589","0.12831339":"0.00111412","0.12835085":"0.50000000","0.12835737":"15.58149719","0.12837370":"0.04762724","0.12840000":"0.01838006","0.12844150":"0.05414246","0.12848288":"2.00000000","0.12850000":"0.12636656","0.12853000":"0.47350871","0.12860000":"0.01835147","0.12870000":"0.01833721","0.12875557":"0.19593054","0.12878000":"0.01474986","0.12880000":"0.01832298","0.12888500":"0.24696838","0.12890000":"0.01830876","0.12900000":"2.17227369","0.12906003":"0.15000000","0.12910000":"0.01828040","0.12916301":"0.00600000","0.12920000":"0.01826625","0.12925485":"0.01000000","0.12930000":"0.24745212","0.12930720":"1.00000000","0.12934872":"0.91234578","0.12937805":"0.00324280","0.12940000":"0.01823802","0.12944500":"0.07988000","0.12950000":"0.01822393","0.12950777":"0.00772154","0.12951000":"1.00000000","0.12952883":"0.02063000","0.12960000":"1.21820987","0.12963172":"1.00000000","0.12967750":"0.00663477","0.12970000":"0.01819583","0.12970700":"0.05000000","0.12980000":"0.01818181","0.12984890":"38.50629166","0.12988999":"0.01592600","0.12989990":"1.00000000","0.12990000":"3.79697992","0.12996000":"28.45250787","0.13000000":"105.57790535","0.13001000":"3.55000000","0.13014018":"0.20014884","0.13026000":"5.26010609","0.13053867":"0.05000000","0.13060000":"0.01521170","0.13063015":"0.00864502","0.13063500":"0.19970003","0.13065000":"3.00000000","0.13065453":"1.68800000","0.13068000":"0.15488215","0.13070000":"0.02890000","0.13070588":"0.01062641","0.13085000":"0.01261000","0.13100000":"0.55481852","0.13119000":"1.42624695","0.13127526":"0.00076747","0.13140000":"4.99250000","0.13142218":"0.53531260","0.13145110":"0.04795989","0.13156400":"0.10983500","0.13190380":"0.04954713","0.13199990":"0.85000000","0.13200000":"0.04813707","0.13207985":"2.80582609","0.13220000":"0.10000000","0.13223250":"0.03581011","0.13226500":"1.49672623","0.13250501":"1.87041741","0.13255000":"0.00431007","0.13259000":"0.00326625","0.13265400":"1.00000000","0.13269997":"0.00406895","0.13270000":"11.01642670","0.13270509":"0.01311569","0.13280000":"25.00000000","0.13286064":"0.00997500","0.13290000":"9.00000000","0.13300000":"1.94197030","0.13301245":"0.16137709","0.13306724":"10.00000003","0.13312022":"0.00075195","0.13320000":"13.02646185","0.13323929":"0.00075803","0.13330000":"0.02830000","0.13333333":"0.24119951","0.13340000":"5.00343116","0.13357001":"1.78771882","0.13362706":"0.49800000","0.13374409":"0.30000000","0.13375800":"0.50000000","0.13377000":"0.00147980","0.13381700":"0.59855702","0.13383770":"0.00131172","0.13393404":"0.83199100","0.13400000":"2.27937441","0.13420000":"0.10000000","0.13435107":"0.01435107","0.13440000":"0.54015115","0.13449999":"0.05000000","0.13450000":"0.01754646","0.13453771":"0.00743286","0.13455590":"0.02000000","0.13456000":"1.00000000","0.13457028":"0.30015673","0.13458440":"0.05000000","0.13461000":"0.04100000","0.13464934":"3.44052877","0.13470000":"0.02810000","0.13479900":"0.00997500","0.13482984":"37.08377633","0.13489990":"1.00000000","0.13497000":"0.31329197","0.13500000":"13.45704002","0.13520000":"0.00849900","0.13545151":"2.36300000","0.13550000":"0.23182039","0.13553867":"0.10000000","0.13555000":"0.03000000","0.13556000":"0.23882517","0.13575000":"1.84651474","0.13580000":"0.01000000","0.13583500":"0.00550387","0.13588888":"3.42518025","0.13592600":"0.74775123","0.13595501":"1.00000000","0.13598001":"1.60000000","0.13598660":"0.14954400","0.13599930":"1.00000000","0.13600000":"14.41773742","0.13600984":"0.10621308","0.13601500":"0.07831213","0.13620000":"0.10000000","0.13627549":"0.02000000","0.13632000":"0.08556493","0.13645000":"0.02000000","0.13650000":"0.09638937","0.13661860":"0.03689903","0.13681000":"1.00000000","0.13700000":"0.00775193","0.13715000":"0.03847000","0.13730000":"0.02750000","0.13738711":"0.22067385","0.13750000":"1.06716363","0.13753000":"0.35000000","0.13762439":"0.02065710","0.13777000":"2.00000000","0.13784123":"0.33261107","0.13797015":"2.02164013","0.13800000":"0.29784446","0.13801493":"0.82179763","0.13815910":"0.05824446","0.13820000":"0.10000000","0.13840002":"0.05613856","0.13840337":"28.70423164","0.13840997":"0.03000000","0.13850000":"0.01703971","0.13858609":"0.10972500","0.13870000":"0.02730000","0.13870570":"0.10000000","0.13872138":"0.51143309","0.13876706":"0.02090443","0.13880000":"0.00500000","0.13881459":"0.00714241","0.13890000":"0.01000000","0.13899552":"0.05000000","0.13900000":"1.53259728","0.13910915":"0.00719934","0.13929876":"0.01449354","0.13930000":"0.75000000","0.13934000":"0.04000000","0.13938899":"0.00997500","0.13942084":"0.17764552","0.13944600":"0.94597819","0.13949788":"2.00000000","0.13950000":"0.04954906","0.13955500":"0.08973911","0.13959165":"0.00081977","0.13960000":"8.00000000","0.13969241":"0.00715858","0.13972399":"0.84980637","0.13977044":"0.50000000","0.13979700":"0.15008820","0.13983482":"35.75647069","0.13985523":"0.06508622","0.13989000":"3.72984442","0.13989990":"1.00000000","0.13990000":"0.99639255","0.13991704":"0.04251787","0.13999900":"0.09324583","0.14000000":"71.45630775","0.14000001":"0.00755827","0.14000029":"0.00640166","0.14021001":"0.00792863","0.14033400":"0.00997500","0.14040000":"0.08500000","0.14050000":"0.07619296","0.14060000":"0.07244009","0.14069999":"0.02094713","0.14072500":"0.00071066","0.14080000":"0.29955000","0.14083000":"0.01885285","0.14083001":"0.00326618","0.14086895":"0.35000000","0.14087007":"0.59412531","0.14100000":"0.99158876","0.14100902":"0.12238308","0.14110010":"2.39019194","0.14121844":"0.00644820","0.14130000":"0.02670000","0.14140000":"0.02624420","0.14142395":"42.48936173","0.14147000":"0.08243544","0.14169451":"0.07193804","0.14177503":"0.40179320","0.14179999":"0.08892158","0.14182418":"5.00000000","0.14191110":"0.00142346","0.14191672":"5.33069002","0.14200000":"9.01728363","0.14200037":"0.01314758","0.14202299":"0.39812218","0.14220758":"0.50905612","0.14225000":"0.63895091","0.14228610":"0.00359296","0.14237000":"1.00000000","0.14246951":"1.44500000","0.14247500":"0.00431014","0.14250000":"0.00998500","0.14255202":"0.97979656","0.14260000":"0.03599027","0.14261146":"0.00126217","0.14268388":"0.12954539","0.14270000":"0.02650000","0.14273010":"0.01454489","0.14274409":"0.46000000","0.14277502":"0.90876795","0.14280000":"30.00000000","0.14289900":"0.00997500","0.14295000":"0.33277963","0.14295873":"0.00189525","0.14300000":"44.18890963","0.14300010":"0.11206228","0.14301550":"0.00997500","0.14316138":"0.15000000","0.14318824":"5.00000000","0.14320000":"4.01821746","0.14332828":"0.06609464","0.14336000":"0.18601190","0.14340000":"0.00597655","0.14349999":"0.00430111","0.14350000":"0.00593448","0.14370000":"0.00131644","0.14372100":"4.33011244","0.14380000":"7.92993901","0.14387499":"0.34917736","0.14390000":"0.20000000","0.14400000":"8.45471184","0.14400711":"0.01885440","0.14420886":"0.37088035","0.14440000":"0.13500000","0.14440557":"0.20514420","0.14442750":"0.00997500","0.14444000":"0.04000000","0.14445677":"0.50000000","0.14449999":"0.05000000","0.14450000":"1.30000000","0.14452160":"11.42408361","0.14455590":"0.02000000","0.14458440":"0.05510526","0.14464644":"0.09985000","0.14470000":"0.05000000","0.14481000":"0.00236483","0.14483983":"34.52088873","0.14485000":"0.00652720","0.14486000":"0.10000000","0.14489534":"0.01760305","0.14490000":"0.62217527","0.14497281":"0.00689785","0.14497800":"0.01865879","0.14500000":"17.96278636","0.14500090":"0.44147681","0.14501950":"0.01034343","0.14510596":"0.30104097","0.14514381":"0.15507012","0.14516500":"1.00000000","0.14521000":"0.66186537","0.14530000":"0.02600000","0.14530411":"0.04992500","0.14531818":"0.01227150","0.14532110":"0.48891415","0.14540000":"0.31381746","0.14555499":"0.00866464","0.14560003":"0.00078534","0.14560020":"0.25000000","0.14564987":"0.00145349","0.14566000":"0.02649328","0.14567800":"1.00000000","0.14591033":"2.00000000","0.14592663":"0.19950000","0.14592948":"0.00068595","0.14600000":"0.92536553","0.14602620":"0.22241656","0.14615270":"0.01313189","0.14638000":"8.96803312","0.14647500":"2.18390000","0.14660000":"0.32475122","0.14670000":"0.02580000","0.14670402":"0.07480553","0.14691033":"2.00000000","0.14700000":"2.32052800","0.14702569":"0.05000000","0.14704000":"0.18135655","0.14714849":"0.00997500","0.14720000":"0.01000000","0.14731910":"0.37314283","0.14746004":"6.88965000","0.14749739":"3.79595400","0.14750000":"0.09459335","0.14752884":"0.01124875","0.14800000":"5.30117354","0.14819160":"1.99700000","0.14829879":"0.00631306","0.14842000":"1.00000000","0.14849000":"0.00407975","0.14850000":"0.06839042","0.14870000":"6.05300000","0.14870570":"0.10000000","0.14880000":"0.99911452","0.14890000":"0.10564961","0.14897578":"1.12170049","0.14900000":"5.01944078","0.14902800":"0.20000000","0.14908386":"0.74237912","0.14930000":"0.02530000","0.14941218":"0.00106645","0.14943000":"1.00000000","0.14956689":"0.01892109","0.14970000":"0.20852935","0.14971354":"0.00111315","0.14975000":"0.20000000","0.14978000":"1.72856217","0.14979000":"0.01000000","0.14984839":"33.36705631","0.14985687":"6.88705389","0.14995000":"0.05000000","0.14995014":"1.00000000","0.14998019":"0.50000000","0.14998989":"40.00269618","0.14999002":"0.20973542","0.14999900":"0.03609309","0.14999980":"30.00000000","0.14999999":"0.19022715","0.15000000":"111.14746160","0.15000001":"0.69999998","0.15004618":"0.03249287","0.15010000":"0.01000000","0.15019897":"0.05245208","0.15020000":"1.31312028","0.15033932":"0.00985329","0.15037000":"0.00559915","0.15037981":"0.00664983","0.15050000":"0.00167902","0.15053325":"0.16796919","0.15064075":"0.00133007","0.15070000":"0.02510000","0.15072809":"0.15000000","0.15073889":"0.01535831","0.15090000":"0.05000000","0.15092636":"1.23181869","0.15100000":"12.18448944","0.15101015":"0.00069500","0.15102604":"0.00537981","0.15140000":"1.02585617","0.15145000":"1.50000000","0.15150000":"0.03985279","0.15160000":"2.94210482","0.15170514":"0.00500000","0.15173441":"7.07635045","0.15175000":"30.10771200","0.15179791":"0.07283776","0.15199900":"1.48405104","0.15200000":"0.66510672","0.15212000":"0.00672062","0.15222198":"0.50459917","0.15230000":"0.15266350","0.15247470":"0.02649295","0.15250000":"3.49805607","0.15280000":"35.00000000","0.15281000":"0.10000000","0.15296640":"0.00071690","0.15300000":"0.00697196","0.15316510":"0.01321681","0.15330000":"0.02470000","0.15340000":"0.02925905","0.15347681":"6.65139524","0.15349919":"0.05164980","0.15350000":"0.06848261","0.15353761":"9.98500000","0.15370000":"0.00130893","0.15382880":"0.15000000","0.15399999":"2.23546279","0.15400000":"1.62893907","0.15400001":"1.67720199","0.15420000":"0.24553276","0.15449999":"12.15678883","0.15462174":"0.03652092","0.15470000":"0.02440000","0.15489848":"32.27920428","0.15498989":"23.24039935","0.15500000":"7.71518924","0.15500001":"0.27100000","0.15511100":"0.01744123","0.15520005":"0.00900000","0.15529326":"0.21744768","0.15540000":"0.05242594","0.15559050":"0.02460857","0.15591431":"0.00641378","0.15598800":"0.00138333","0.15599930":"1.00000000","0.15600000":"0.20045775","0.15600001":"0.21706521","0.15620818":"0.72797520","0.15639814":"0.05000001","0.15681235":"0.36709559","0.15700000":"1.50680272","0.15730000":"0.02400000","0.15743488":"0.09985000","0.15750000":"0.06400000","0.15752000":"2.11746294","0.15762772":"0.02000000","0.15770000":"0.00600000","0.15780000":"0.24096283","0.15800000":"0.00736581","0.15846796":"0.03000000","0.15850000":"0.00864491","0.15853000":"0.01927868","0.15860000":"0.02380000","0.15888888":"0.10000000","0.15899993":"0.00104260","0.15900000":"1.55671140","0.15909276":"0.12974000","0.15925405":"0.01000000","0.15948984":"31.34995738","0.15949490":"0.05000000","0.15957497":"0.30000000","0.15976997":"6.00000000","0.15989845":"0.01438413","0.15991201":"0.99928896","0.15995000":"0.04985000","0.16000000":"21.45155242","0.16010000":"1.50000000","0.16010001":"0.50000000","0.16024964":"0.00063027","0.16028596":"0.67000000","0.16050000":"0.00934579","0.16073268":"0.00714241","0.16100000":"0.00719424","0.16147558":"2.72191618","0.16151266":"0.11975999","0.16157716":"0.00618899","0.16188000":"0.12354831","0.16189125":"0.35377860","0.16190000":"3.98500000","0.16200000":"1.00849328","0.16203890":"0.01410722","0.16238453":"0.16770526","0.16276999":"0.18981000","0.16280000":"30.00000000","0.16285328":"2.04126348","0.16300000":"0.00729927","0.16318194":"0.10000000","0.16400000":"0.00859983","0.16449999":"0.07089333","0.16452100":"5.00000000","0.16466803":"2.75536306","0.16473000":"0.50000000","0.16485948":"30.32885763","0.16500000":"3.15988842","0.16500100":"0.30000000","0.16524901":"0.00286901","0.16550000":"0.02239847","0.16557879":"0.20059622","0.16568376":"0.04599998","0.16600000":"0.18141208","0.16649230":"0.01410260","0.16653020":"13.00000000","0.16718283":"0.11970000","0.16736922":"0.00597481","0.16750000":"0.00149253","0.16777777":"0.59602649","0.16800000":"0.03124689","0.16803566":"0.11970000","0.16820000":"0.11970000","0.16832300":"0.22690900","0.16870570":"0.10000000","0.16875000":"0.42500000","0.16880000":"0.30033173","0.16885808":"29.61066477","0.16888888":"0.10000000","0.16900000":"0.05499250","0.16919983":"0.11970000","0.16925400":"0.17780189","0.16930000":"3.00000000","0.16949000":"0.00059009","0.16964528":"0.02000000","0.16976997":"6.00000000","0.16979003":"0.05172406","0.16997050":"0.11970000","0.16999900":"0.05000000","0.17000000":"4.67799042","0.17000001":"0.07191201","0.17004900":"0.28499955","0.17013559":"0.12258726","0.17048000":"0.15642109","0.17050130":"1.00848500","0.17050505":"0.00253365","0.17110002":"0.33858881","0.17200000":"0.00124689","0.17211133":"0.07445443","0.17227000":"29.67561759","0.17249849":"0.43260956","0.17329128":"0.00577063","0.17352583":"0.11970000","0.17387440":"1.49987844","0.17400000":"0.00124689","0.17424000":"0.15304561","0.17440000":"3.23930177","0.17456000":"0.00995500","0.17466530":"2.96112636","0.17467977":"0.01035115","0.17480259":"28.60369354","0.17500000":"3.10656046","0.17521300":"0.17973029","0.17562267":"0.04400001","0.17600000":"10.00124689","0.17692509":"0.10972500","0.17739698":"1.12331250","0.17760000":"1.00000000","0.17770000":"0.01300000","0.17799000":"0.02000000","0.17799671":"0.21551144","0.17800000":"4.02377066","0.17884802":"27.95669598","0.17887212":"4.00000000","0.17888880":"0.10000000","0.17899993":"0.12980500","0.17900000":"0.05000000","0.17934413":"0.00557587","0.17971001":"0.07727524","0.17993995":"0.00500000","0.17997697":"4.00000000","0.17999990":"0.00253854","0.17999999":"0.07000000","0.18000000":"33.93239228","0.18200000":"30.00124689","0.18240500":"0.01101334","0.18254698":"0.00054900","0.18265078":"0.00714241","0.18299000":"0.00433900","0.18396000":"0.10871929","0.18400000":"0.00124689","0.18400001":"0.12087515","0.18442500":"0.15252597","0.18450000":"0.04593125","0.18455555":"1.00000000","0.18466390":"1.62309433","0.18473000":"0.50000000","0.18478848":"27.05796233","0.18484029":"1.50000000","0.18500000":"1.01500000","0.18528914":"0.11674959","0.18543000":"2.34995463","0.18552855":"0.00539001","0.18580011":"20.00000000","0.18595457":"0.00675408","0.18600000":"0.00124689","0.18624000":"0.10738832","0.18667999":"6.38381646","0.18675000":"0.00113673","0.18675241":"0.04199999","0.18690875":"0.13916377","0.18700000":"0.15092225","0.18720767":"1.99021784","0.18770000":"0.05000000","0.18776997":"6.00000000","0.18777777":"0.53254438","0.18800000":"0.00124689","0.18880000":"0.00079800","0.18888880":"0.10000000","0.18900000":"0.05000000","0.18910100":"5.29229760","0.18932298":"0.43546927","0.18947884":"26.38816945","0.18949490":"0.05153542","0.18957956":"0.00664628","0.18977613":"0.07000000","0.19000000":"5.73579316","0.19010831":"10.00000000","0.19030616":"0.00470000","0.19163004":"0.05442915","0.19174800":"0.04992500","0.19184528":"0.00521253","0.19200000":"0.00124689","0.19230080":"0.04000000","0.19257150":"0.03990000","0.19290010":"33.54529642","0.19297994":"17.38426744","0.19340003":"0.05894024","0.19393300":"5.00000000","0.19400000":"0.01124689","0.19440000":"0.11300000","0.19449999":"0.05000000","0.19462007":"0.08427245","0.19473000":"1.00000000","0.19485900":"0.05000000","0.19494788":"25.64787955","0.19500000":"1.00000000","0.19599600":"0.08771546","0.19600000":"1.05634893","0.19670000":"0.00998500","0.19673799":"6.14516633","0.19691031":"0.92572112","0.19751799":"0.00192109","0.19780000":"0.17000000","0.19800000":"0.00124689","0.19824997":"0.00200100","0.19829504":"0.00504299","0.19881000":"0.49925000","0.19881564":"0.04000002","0.19900000":"0.05000000","0.19920201":"0.24809239","0.19949478":"25.06331118","0.19999698":"0.50000000","0.19999999":"1.07563615","0.20000000":"46.21030415","0.20000400":"0.02630687","0.20020000":"5.00000000","0.20122808":"0.00439998","0.20149980":"0.09975000","0.20152000":"10.00000000","0.20170380":"0.00201587","0.20200000":"0.00124689","0.20235000":"0.49950000","0.20270890":"0.63900000","0.20400000":"0.00124689","0.20456887":"0.00714241","0.20462000":"1.21035607","0.20487852":"0.00488094","0.20488025":"24.40449852","0.20500000":"0.00500000","0.20600000":"0.00124689","0.20673799":"4.35000000","0.20797200":"10.42936396","0.20800000":"0.00124689","0.20856320":"0.02157619","0.20885793":"0.02237690","0.20900000":"0.12000000","0.20948802":"23.86771216","0.21000000":"5.11063576","0.21150000":"0.00200000","0.21159641":"0.00472598","0.21189797":"0.03799998","0.21200000":"0.00124689","0.21235300":"1.88512217","0.21244688":"0.04561500","0.21249536":"0.05000000","0.21340108":"0.00409997","0.21400000":"0.00094392","0.21473000":"1.00000000","0.21473799":"4.30000000","0.21489488":"23.26718891","0.21500000":"0.70000000","0.21584000":"0.12354831","0.21600000":"2.99650878","0.21748590":"0.03000000","0.21797996":"4.35000000","0.21800000":"0.00337117","0.21844936":"0.00457772","0.21888880":"0.45685297","0.21894880":"22.83638811","0.21897368":"0.33312910","0.21900000":"0.37275000","0.22000000":"10.06730212","0.22190000":"0.04265331","0.22200000":"0.00338294","0.22222222":"0.88362824","0.22273599":"1.14124105","0.22400000":"0.00338009","0.22489488":"22.23260831","0.22500000":"0.03000000","0.22543801":"0.00443581","0.22645568":"0.03599998","0.22648696":"0.00714242","0.22740000":"0.02982525","0.22800000":"30.51572391","0.22880858":"0.00379998","0.22894894":"21.83892960","0.22900000":"0.05000000","0.22995225":"0.01994000","0.23256298":"0.00429991","0.23473000":"1.00000000","0.23482894":"21.29209368","0.23848532":"1.68025853","0.23894828":"20.92502943","0.23900000":"0.05000000","0.23982486":"0.00416971","0.24000000":"0.33621999","0.24070720":"0.06669369","0.24182418":"5.00000000","0.24221403":"0.03400001","0.24318849":"4.99500001","0.24390010":"30.00000000","0.24435256":"0.00340000","0.24483894":"20.42158734","0.24500040":"2.65619743","0.24528000":"0.10871929","0.24667328":"0.00045000","0.24722423":"0.00404491","0.24777777":"0.40358744","0.24832000":"0.10738832","0.24885793":"0.03000000","0.24889000":"3.00000000","0.24889775":"0.10000000","0.24900000":"0.05000000","0.25000000":"42.35645600","0.25078490":"0.05000000","0.25105743":"4.39997108","0.25150000":"0.00200000","0.25185152":"0.00514276","0.25278000":"0.69895000","0.25399679":"2.00000000","0.25476165":"0.00392524","0.25500000":"0.50000000","0.25589000":"0.00697297","0.25622530":"0.00604277","0.25681496":"0.00413846","0.25900000":"0.05000000","0.25954820":"0.03199999","0.26000000":"0.04405246","0.26024598":"11.60873204","0.26105680":"0.00397381","0.26243767":"0.00381043","0.26318999":"0.17263558","0.26367467":"0.00299998","0.26507849":"0.05000000","0.26690000":"0.45180000","0.26755000":"4.20000000","0.26900000":"0.05000000","0.26917018":"0.03000000","0.27000000":"1.52218032","0.27025280":"0.00370024","0.27061800":"2.77144268","0.27390010":"20.00000000","0.27474704":"0.08277851","0.27570007":"0.03767063","0.27650784":"0.05000000","0.27700000":"2.87837328","0.27776130":"0.04071570","0.27820755":"0.00359444","0.27900000":"0.05000000","0.28000000":"10.67141709","0.28150000":"0.00300000","0.28275312":"2.00000000","0.28280000":"27.80052016","0.28284791":"21.24468086","0.28289114":"1.07409933","0.28483695":"0.00270000","0.28630242":"0.00349281","0.28637648":"0.90099540","0.28765078":"0.04881565","0.28882875":"0.01995000","0.28888880":"0.34615395","0.28900000":"0.05000000","0.29000000":"0.00139511","0.29200000":"0.11538835","0.29245454":"0.05000007","0.29453787":"0.00339515","0.29500000":"2.16720000","0.29530980":"0.01987147","0.29637648":"1.00000000","0.29800000":"1.69214686","0.29900000":"1.00467775","0.29970100":"0.00300000","0.29999900":"1.00000000","0.30000000":"15.92541655","0.30221420":"0.00036728","0.30265465":"0.50000000","0.30291436":"0.00330126","0.30292911":"1.84263363","0.30637648":"1.00000000","0.30900000":"0.12000000","0.31000000":"5.06295713","0.31143234":"0.00321097","0.31245454":"0.05000007","0.31447750":"0.00240001","0.31637648":"1.00000000","0.31655000":"0.00431915","0.31900000":"0.05000000","0.32000000":"0.06641610","0.32637648":"1.00000000","0.32800000":"46.68429950","0.32960000":"9.31931600","0.33060000":"0.38958241","0.33280168":"0.33280168","0.33340000":"6.00000000","0.33637648":"1.00000000","0.34055000":"1.00000000","0.34473000":"1.00000000","0.34637648":"1.00000000","0.34885793":"0.02000000","0.35000000":"0.40000000","0.35637648":"1.00000000","0.35700000":"0.02647901","0.35793500":"0.00031011","0.36637648":"1.00000000","0.37000000":"0.49900000","0.37637648":"1.00000000","0.38828000":"50.00000000","0.39248121":"0.00028281","0.39600004":"1.20000000","0.40000000":"12.34555388","0.40900000":"0.01744356","0.41899100":"0.00026492","0.42885990":"0.00357829","0.42906810":"0.00025871","0.43000000":"0.01146596","0.43888888":"0.22784810","0.44473000":"1.00000000","0.44964644":"1.00000000","0.45500000":"0.30000000","0.47793131":"0.00023226","0.48000000":"0.01020276","0.48280000":"109.90319469","0.48554620":"0.03792183","0.49000000":"4.41113744","0.49249264":"0.04465203","0.49500000":"2.00000000","0.49899490":"0.00022244","0.50000000":"22.14493397","0.51327180":"0.00373294","0.52535000":"0.00021128","0.53795003":"1.00000000","0.54473000":"1.00000000","0.55468000":"0.00020011","0.55500000":"0.50000000","0.58112070":"0.10000000","0.58830030":"0.72500000","0.58832390":"0.00018867","0.60000000":"0.74095075","0.61080100":"0.00018172","0.61279990":"1.74434023","0.63446989":"0.44249357","0.63851000":"0.00929285","0.64150000":"0.00017303","0.64730000":"1.00000000","0.65000000":"0.30000000","0.65888888":"0.30354131","0.66455020":"0.00016703","0.66666666":"0.04080688","0.67777770":"4.94978580","0.68775010":"0.00016139","0.68888888":"5.13949362","0.69999999":"4.97950728","0.70000000":"0.68455275","0.71480000":"0.07699578","0.71850025":"0.41952015","0.72300000":"0.01999462","0.72590020":"0.00015291","0.73775010":"1.14880974","0.74500000":"2.00000000","0.74700400":"0.20900019","0.75000000":"0.45381243","0.76551230":"0.00400000","0.76888190":"0.00154049","0.77000000":"0.34455224","0.80000000":"0.79487747","0.83700000":"0.01997000","0.84643808":"1.00000000","0.84730000":"2.00000000","0.85000000":"0.30000000","0.87745501":"0.24168029","0.87888888":"8.04623220","0.88425001":"0.00012002","0.89000000":"0.28872521","0.90000000":"2.82396143","0.91000000":"0.00995589","0.94730000":"2.00000000","0.94964644":"1.00000000","0.95000000":"0.30000000","0.96424250":"0.01200000","0.97000000":"1.00000000","0.97799990":"0.01000000","0.97949720":"0.09000000","0.98000000":"0.85628262","0.99000000":"0.52793810","0.99000020":"0.20000000","0.99500000":"1.00000000","0.99900000":"0.21441306","0.99999998":"1.88673282","0.99999999":"2.97890914","1.00000000":"32.39198203","1.02200499":"1.61425538","1.07530000":"0.15000000","1.09880480":"20.09063444","1.10000000":"0.15466832","1.18956449":"0.01917406","1.20000000":"0.22349025","1.30000000":"0.44964486","1.30199353":"1.53651820","1.52300000":"0.00669289","1.59066526":"0.01000000","1.60000000":"0.06197573","1.66666666":"1.00724564","1.83901004":"12.97894124","1.84643808":"1.00000000","1.94730000":"1.00000000","2.00000000":"0.92918454","2.14010402":"0.06925027","2.22222222":"4.95020160","2.45597765":"0.01000000","2.50000000":"2.68557693","3.08952500":"0.50000000","3.20000000":"2.23960244","3.55000505":"4.95061740","3.55555555":"4.95228060","4.00000000":"0.00027500","4.08237033":"0.02801567","4.44444444":"4.94978580","4.93891000":"0.00285286","5.00000000":"2.30051373","5.08299899":"0.01793293","5.94750146":"0.50000000","6.00000000":"1.00000000","6.93333333":"4.95648880","7.00000000":"1.30000000","7.96424250":"0.01288805","8.00000000":"1.00026250","9.00000000":"1.00000000","9.54650098":"0.01000000","9.70000000":"1.00000000","10.00000000":"2.50170455","11.70735686":"0.00081917","12.17500103":"0.00082135","12.17846784":"0.00082112","12.22535581":"0.00081797","15.00000000":"0.30000000","15.94750146":"0.50000000","16.00000000":"0.00019375","20.00000000":"1.00000000","24.97583057":"0.01000000","25.00000000":"0.30000000","32.00000000":"0.00012813","34.00000000":"0.10000000","38.00000000":"0.00322360","50.00000000":"1.00000000","64.00000000":"0.00006266","77.38900000":"0.01000000","84.21800000":"0.00249400","96.35001000":"0.00000234","99.00000000":"0.15000000","100.00000000":"1.10000000","115.94750146":"0.50000000","130.00000000":"0.00003085","197.00000000":"2.00000000","260.00000000":"0.00001543","475.13465857":"0.01500000","520.00000000":"0.00000772","780.00000000":"0.20000000","999.00000000":"0.20000000","1000.00000000":"0.00103379","1000.02022945":"1.00000000","1000.17618025":"0.00100000","1148.00000000":"0.04594689","1997.00000000":"2.00000000","2000.00000000":"0.00000206","3000.00000000":"0.00000137","3772.00000000":"0.65977073","4000.00000000":"0.00000103","5000.00000000":"0.10284089"},{"0.02310611":"21.20361406","0.02310610":"6.68324293","0.02310609":"4.00000000","0.02310602":"2.18632904","0.02310600":"83.88440000","0.02310576":"85.00000000","0.02308342":"3.41194800","0.02306799":"8.00000000","0.02306796":"0.70170097","0.02306400":"5.16659306","0.02306395":"0.00596558","0.02306050":"1.21419744","0.02304842":"0.01189382","0.02304128":"0.02170018","0.02303636":"0.04775060","0.02303635":"0.08985799","0.02303184":"18.29000000","0.02302641":"0.01594362","0.02302407":"0.01209088","0.02300986":"0.59849633","0.02300041":"0.00573903","0.02300000":"18.90503189","0.02298903":"60.57000000","0.02298495":"0.55500003","0.02298317":"56.49775139","0.02297730":"0.00621588","0.02297480":"0.00860000","0.02297456":"0.01189382","0.02295038":"2.52305473","0.02294923":"1.27669578","0.02294920":"0.00599546","0.02294396":"0.05498811","0.02293713":"586.54000000","0.02292989":"17.04080463","0.02292988":"0.04361121","0.02292500":"0.22107450","0.02292491":"0.02181034","0.02292001":"0.05017450","0.02292000":"0.09620419","0.02290070":"0.01189382","0.02290000":"6.00000000","0.02289674":"0.06571171","0.02288425":"0.00624884","0.02288000":"163.40463321","0.02287368":"1.22411436","0.02287003":"0.55500004","0.02286000":"0.00500000","0.02285900":"0.00950063","0.02285000":"7.22107450","0.02284584":"0.00485865","0.02283700":"0.59849633","0.02283502":"0.00602532","0.02282684":"0.01189382","0.02280367":"0.05262311","0.02280366":"0.10261511","0.02278000":"2.00000000","0.02277500":"0.22107450","0.02277463":"11.29396482","0.02276809":"0.00676386","0.02276166":"0.05492789","0.02275568":"0.55500005","0.02275298":"0.01189382","0.02274692":"302.63000000","0.02272141":"0.00605574","0.02270000":"11.22596436","0.02269422":"0.22032041","0.02268732":"0.05509686","0.02268731":"0.10909183","0.02268000":"4.00000000","0.02267912":"0.01189382","0.02267775":"175.57000000","0.02267333":"0.00500000","0.02266543":"0.59849633","0.02265900":"0.12864910","0.02265192":"0.00728416","0.02264190":"0.55500006","0.02263528":"0.06571171","0.02262933":"0.02377825","0.02262500":"0.22107450","0.02260836":"0.00608560","0.02260526":"0.01189382","0.02260000":"4.00000000","0.02257230":"2.22181809","0.02257098":"0.05759609","0.02257097":"0.11563527","0.02256588":"0.02869842","0.02255000":"0.22107450","0.02254385":"0.14110012","0.02253576":"0.00780981","0.02253321":"119.34000000","0.02253140":"0.01189382","0.02253000":"5.00000000","0.02252869":"0.55500007","0.02250000":"18.85347779","0.02249588":"0.00611602","0.02249515":"0.59849633","0.02248666":"0.00500000","0.02248000":"0.00493772","0.02247500":"0.22107450","0.02245754":"0.01189382","0.02245698":"1.11412131","0.02244206":"0.06015312","0.02243992":"0.05310000","0.02243186":"0.10510096","0.02241786":"0.05000000","0.02241605":"0.55500008","0.02241595":"0.10074969","0.02240130":"0.05000000","0.02240100":"0.01071381","0.02240000":"5.23566379","0.02238467":"0.90797261","0.02238396":"0.00614698","0.02238368":"0.01189382","0.02237698":"0.63312163","0.02237383":"0.06571171","0.02236391":"0.88417429","0.02235587":"0.05000000","0.02233010":"0.05544087","0.02232615":"0.59849633","0.02232500":"0.22107450","0.02231034":"0.28013916","0.02230982":"0.01189382","0.02230886":"0.05310000","0.02230769":"0.03441200","0.02230630":"1.03689003","0.02230400":"1.04147597","0.02230397":"0.55500009","0.02230390":"9.93529472","0.02230000":"11.00948431","0.02227259":"0.00617738","0.02227109":"0.05030075","0.02226251":"0.05560919","0.02226000":"0.00498653","0.02225000":"0.22107450","0.02223596":"0.01189382","0.02221312":"0.05000000","0.02220100":"0.01081033","0.02220007":"1.05210930","0.02220000":"11.03168423","0.02219491":"0.05577856","0.02219351":"0.05000000","0.02219245":"0.55500010","0.02218276":"0.00494168","0.02217500":"0.22107450","0.02216829":"0.05921791","0.02216210":"0.01189382","0.02216178":"0.00620834","0.02215841":"0.59849633","0.02215279":"0.05000000","0.02215000":"0.00902935","0.02214275":"0.06800000","0.02212731":"0.05594897","0.02211237":"0.06571171","0.02210700":"0.05000000","0.02210000":"12.02704509","0.02209310":"0.00677973","0.02208149":"0.55500011","0.02207692":"0.03441200","0.02207220":"7.06771414","0.02205971":"0.05612042","0.02205152":"0.00623930","0.02205000":"25.00000000","0.02204544":"0.00503505","0.02202786":"2.26985193","0.02202500":"0.22107450","0.02202000":"0.04528020","0.02201232":"1.00000000","0.02201000":"0.02057701","0.02200000":"28.91081357","0.02199194":"0.59849633","0.02197108":"0.55500012","0.02195001":"0.00618633","0.02195000":"0.22107450","0.02194181":"0.00627084","0.02194016":"0.06583622","0.02190001":"0.01506849","0.02190000":"10.04899443","0.02188891":"0.05165706","0.02187500":"0.22107450","0.02187244":"0.03400000","0.02187190":"0.25146413","0.02186122":"0.55500013","0.02185376":"0.20000000","0.02185092":"0.06571171","0.02184795":"1.00000000","0.02184615":"0.03441200","0.02183847":"0.84955539","0.02183264":"0.00630238","0.02182672":"0.59849633","0.02180999":"25.10000000","0.02180100":"0.01032063","0.02180000":"8.24419836","0.02179599":"4.58799859","0.02179570":"59.21000000","0.02178747":"0.00880783","0.02178623":"0.05000000","0.02178526":"0.05000000","0.02176000":"0.03000000","0.02175608":"1.00000000","0.02175191":"0.55500014","0.02175007":"0.03400000","0.02174720":"1.00000000","0.02172723":"0.13807604","0.02172401":"0.00633334","0.02170100":"0.01036819","0.02170037":"7.00000000","0.02170000":"6.05667373","0.02169971":"0.36002785","0.02166998":"0.00512229","0.02166274":"0.59849633","0.02165071":"1.00000000","0.02164315":"0.55500015","0.02161593":"0.00636546","0.02161538":"0.03441200","0.02160312":"60.58000000","0.02160100":"0.01835285","0.02160000":"6.06995926","0.02159205":"0.02623049","0.02158095":"0.00889211","0.02156660":"1.00000000","0.02155740":"0.05000000","0.02155174":"0.03314292","0.02152986":"0.05000000","0.02152751":"0.46452190","0.02152000":"4.54030457","0.02151399":"1.00000000","0.02151300":"10.00000000","0.02150838":"0.00639700","0.02150597":"0.05000000","0.02150100":"0.01813870","0.02150000":"7.40403169","0.02147939":"0.00854994","0.02147050":"0.05000000","0.02143826":"0.07209396","0.02143749":"0.05035105","0.02142283":"0.06434257","0.02140758":"0.05000000","0.02140314":"0.47656559","0.02140137":"0.00642912","0.02140100":"0.01612075","0.02140000":"6.05654206","0.02139989":"1.00000000","0.02138461":"0.03441200","0.02137470":"0.10200000","0.02136621":"0.83008660","0.02136230":"0.00708870","0.02135061":"0.21364635","0.02133295":"1.27000000","0.02132200":"1.28621424","0.02130100":"0.01278719","0.02130000":"7.05774648","0.02129489":"0.00646126","0.02129268":"0.14089395","0.02127842":"0.00901854","0.02125320":"1.00000000","0.02124505":"0.00522474","0.02123000":"0.03000000","0.02122823":"0.41447650","0.02122080":"1.24121192","0.02121000":"0.05000000","0.02120808":"1.00000000","0.02120100":"0.01282959","0.02120000":"16.38709624","0.02118500":"0.24923484","0.02117721":"0.03400000","0.02116300":"0.06000000","0.02116140":"0.05050226","0.02116091":"0.05055276","0.02115384":"0.03441200","0.02115176":"0.02836642","0.02115000":"1.47487375","0.02113953":"0.05000000","0.02112575":"0.05000000","0.02111506":"0.05000000","0.02111285":"0.05000000","0.02110803":"1.00000000","0.02110687":"0.05000000","0.02110579":"0.46269886","0.02110504":"0.05000000","0.02110100":"0.01184778","0.02110000":"6.05540427","0.02109532":"0.05000000","0.02109374":"0.05000000","0.02109287":"0.05000000","0.02107989":"0.05000000","0.02106522":"0.89500000","0.02105417":"0.10189431","0.02105237":"0.03400000","0.02105156":"1.00000000","0.02103558":"0.79650000","0.02101335":"0.09298261","0.02100100":"0.01000000","0.02100000":"19.24285681","0.02096572":"0.03400000","0.02094990":"20.00000000","0.02094415":"0.10030681","0.02093636":"0.07898980","0.02093596":"0.05000000","0.02093544":"0.05000000","0.02093209":"0.05000000","0.02092527":"0.05000000","0.02092307":"0.03441200","0.02091527":"0.05000000","0.02090100":"0.06000000","0.02090000":"8.01104498","0.02088870":"0.00879171","0.02088799":"0.00918711","0.02088502":"0.05000000","0.02086682":"0.14376939","0.02086659":"1.00000000","0.02086356":"0.03400000","0.02085000":"30.00000000","0.02084167":"0.03698696","0.02083991":"0.00532631","0.02083214":"0.05000000","0.02083179":"0.05000000","0.02083165":"0.05000000","0.02083114":"0.05000000","0.02083113":"0.05000000","0.02083085":"0.05000000","0.02083040":"0.05000000","0.02083011":"0.05000000","0.02082989":"0.05000000","0.02082615":"0.05000000","0.02082506":"0.05000000","0.02082227":"0.05000000","0.02080967":"1.00000000","0.02080100":"0.01000000","0.02080000":"32.02124327","0.02077831":"0.29116899","0.02077375":"3.21333751","0.02076769":"10.48825651","0.02075001":"0.20000000","0.02072925":"0.05000000","0.02072742":"0.05000000","0.02071796":"0.03400000","0.02071073":"0.05000000","0.02071000":"0.01000000","0.02070751":"0.05000000","0.02070339":"3.55741666","0.02070026":"0.05000000","0.02070000":"6.09865991","0.02069230":"0.03441200","0.02067440":"0.48368998","0.02063041":"0.96944269","0.02062611":"0.03400000","0.02060751":"0.19191353","0.02060172":"0.91142328","0.02060100":"0.01000000","0.02060000":"7.20258690","0.02059887":"0.05229155","0.02058800":"0.05231916","0.02052341":"0.03400000","0.02052073":"0.03400000","0.02051399":"7.29745895","0.02051135":"1.45940414","0.02051133":"0.56166156","0.02050100":"0.01085703","0.02050000":"23.57444196","0.02046876":"3.55819131","0.02046153":"0.03441200","0.02044948":"0.14670348","0.02043966":"0.15000000","0.02043446":"0.08659499","0.02043429":"0.00543204","0.02040000":"6.04927550","0.02038437":"0.00500383","0.02038100":"3.95417300","0.02036073":"0.00500964","0.02034423":"4.91539862","0.02034299":"0.57929144","0.02034000":"0.50164208","0.02033810":"2.70003590","0.02032200":"1.96831021","0.02031809":"1.58172224","0.02031062":"3.00000000","0.02031046":"1.16059163","0.02031000":"49.23682915","0.02030174":"0.49256812","0.02030100":"0.98517314","0.02030001":"1.47783179","0.02030000":"14.37874817","0.02027825":"0.10216661","0.02027417":"0.98647689","0.02025363":"0.01602951","0.02025271":"0.00942985","0.02025034":"0.40129040","0.02023467":"3.68155680","0.02023076":"0.03441200","0.02022710":"0.03400000","0.02022379":"0.00908077","0.02021100":"2.69796694","0.02021040":"5.28738818","0.02020000":"16.87018070","0.02019381":"0.17000000","0.02018980":"0.74294942","0.02018120":"1.48653202","0.02017970":"22.00000000","0.02017282":"0.05000000","0.02017117":"0.60517219","0.02010000":"18.62560448","0.02008819":"0.00103729","0.02008000":"0.50013944","0.02007506":"9.90000000","0.02007000":"80.00000000","0.02004049":"0.14969743","0.02004000":"0.00553892","0.02002070":"3.69281648","0.02001800":"0.01901900","0.02000663":"0.05005000","0.02000393":"0.03400000","0.02000133":"0.00918176","0.02000100":"10.99070146","0.02000000":"42.16328564","0.01999444":"1.00000000","0.01993256":"0.09498972","0.01991299":"0.03284640","0.01990000":"56.00552764","0.01988324":"0.17069803","0.01987417":"1.00633134","0.01986768":"0.01566111","0.01981798":"0.00926671","0.01980000":"0.02960809","0.01974055":"4.15418548","0.01973602":"6.00000000","0.01973000":"3.00000000","0.01972411":"1.01398745","0.01971660":"0.76078026","0.01970000":"0.03583757","0.01966227":"0.00934010","0.01966151":"0.09257224","0.01964860":"0.07366020","0.01963968":"0.15275248","0.01962512":"1.15769993","0.01961000":"10.00000000","0.01960000":"1.41203929","0.01959822":"0.05158173","0.01956250":"0.66453675","0.01953727":"1.15623472","0.01953685":"0.03400000","0.01952709":"0.00940475","0.01951500":"14.47357674","0.01951447":"4.21866417","0.01951092":"0.62154886","0.01951000":"9.92172681","0.01950006":"0.10200000","0.01950000":"20.97781436","0.01947503":"0.05010005","0.01947417":"1.02700141","0.01943066":"0.10426440","0.01942659":"0.05000000","0.01941899":"2.00000000","0.01940776":"0.00946258","0.01940031":"0.24258330","0.01940000":"26.50481393","0.01938455":"20.00000000","0.01937572":"0.03400000","0.01936771":"0.10290633","0.01930000":"0.01600156","0.01929370":"0.03400000","0.01928888":"1.00000000","0.01927534":"0.10200000","0.01927315":"0.10101203","0.01925450":"1.12181568","0.01924688":"0.15586993","0.01923780":"0.05000000","0.01923474":"0.05000000","0.01923421":"0.06800000","0.01922775":"0.05000000","0.01922377":"0.05000000","0.01922000":"0.03000000","0.01921807":"3.16433793","0.01920000":"1.06973231","0.01917700":"3.00000000","0.01917282":"0.05000000","0.01916979":"0.05000000","0.01916731":"0.87070000","0.01916580":"0.05000000","0.01916304":"0.64972430","0.01916200":"1.00000000","0.01916029":"0.05000000","0.01916000":"0.30793319","0.01914423":"10.44701198","0.01914000":"4.73354232","0.01911119":"0.01205210","0.01911000":"62.32862376","0.01910174":"0.52351251","0.01910100":"1.57059840","0.01910001":"2.61779968","0.01910000":"11.17831572","0.01906731":"0.87080000","0.01906000":"0.04884575","0.01905712":"0.17000000","0.01902000":"0.60000000","0.01901881":"4.34545201","0.01900000":"12.43473062","0.01899798":"0.05000000","0.01898999":"0.31595594","0.01898384":"0.10219482","0.01896731":"0.87090000","0.01896556":"0.03400000","0.01892876":"0.11452100","0.01890000":"17.63528413","0.01886731":"0.87100000","0.01886194":"0.15905097","0.01881997":"0.03400000","0.01881220":"0.00842125","0.01880400":"4.06075835","0.01880000":"1.57714947","0.01879900":"5.33486409","0.01877781":"0.07373011","0.01876731":"0.87110000","0.01875154":"3.40000000","0.01875000":"0.05000000","0.01873862":"0.03400000","0.01871348":"4.38218366","0.01870000":"0.06443102","0.01869667":"1.08423960","0.01867988":"0.03400000","0.01867052":"0.05000000","0.01866731":"0.87120000","0.01864391":"2.00000000","0.01862370":"0.03400000","0.01860000":"0.23519410","0.01857621":"0.05000000","0.01857615":"0.13600000","0.01857337":"0.05000000","0.01856731":"0.87130000","0.01856540":"0.05000000","0.01856000":"0.26939655","0.01855639":"0.05000000","0.01855545":"0.05000000","0.01854624":"0.05000000","0.01854210":"0.16179398","0.01853538":"0.05000000","0.01852058":"0.15602886","0.01851000":"4.70969584","0.01850858":"0.05000000","0.01850700":"21.00000000","0.01850230":"0.05000000","0.01850037":"12.00000000","0.01850000":"3.24427160","0.01848470":"0.16229692","0.01846731":"0.87140000","0.01845677":"0.17500000","0.01845000":"18.75474580","0.01844000":"0.20601000","0.01842686":"0.12587500","0.01840400":"10.00000000","0.01840158":"0.29955471","0.01840000":"0.70266034","0.01839994":"6.11864114","0.01838048":"2.39255369","0.01836731":"0.87150000","0.01836332":"0.17504568","0.01835928":"5.18378778","0.01834190":"0.00918361","0.01833498":"0.05380045","0.01832450":"0.00662992","0.01831100":"0.05000000","0.01830000":"0.02105410","0.01829000":"0.03000000","0.01827270":"0.00924025","0.01826731":"0.87160000","0.01823707":"0.70544227","0.01822211":"1.09756774","0.01821370":"0.00928990","0.01821100":"0.05000000","0.01820700":"5.51384632","0.01820130":"0.30500000","0.01820000":"11.01504781","0.01818180":"1.65000166","0.01818000":"0.05116282","0.01817898":"12.00000000","0.01816731":"0.87170000","0.01815918":"0.17484655","0.01814423":"22.04557593","0.01813000":"0.27578599","0.01812000":"56.36200662","0.01811500":"0.16560916","0.01811100":"0.05000000","0.01811000":"10.10000000","0.01810174":"0.55243308","0.01810156":"0.87010000","0.01810100":"2.09933153","0.01810001":"4.41988707","0.01810000":"8.79628953","0.01807173":"0.08300257","0.01806731":"0.87180000","0.01806700":"0.50000000","0.01803777":"4.76072189","0.01802824":"2385.14685849","0.01802784":"2.94337480","0.01802000":"21.00000000","0.01800761":"0.09543520","0.01800156":"0.87020000","0.01800000":"37.50460388","0.01799447":"1.82000082","0.01799000":"0.08004446","0.01796731":"0.87190000","0.01792497":"0.13845750","0.01790156":"0.87030000","0.01790000":"8.99616407","0.01788385":"3.68200000","0.01788000":"5.00000000","0.01786731":"0.87200000","0.01785719":"0.02800006","0.01785480":"0.00947380","0.01785000":"0.00577031","0.01784000":"0.06850561","0.01782206":"0.59310090","0.01780545":"0.00898601","0.01780156":"0.87040000","0.01780000":"33.11049833","0.01775270":"0.16898894","0.01775000":"0.00597184","0.01770156":"0.87050000","0.01770000":"0.01609492","0.01768900":"2.22056532","0.01768013":"0.29807869","0.01767788":"0.00848519","0.01766528":"0.16609830","0.01765601":"0.16619867","0.01765000":"0.00589236","0.01763000":"0.33285388","0.01762799":"0.00025778","0.01761252":"0.16700629","0.01760156":"0.87060000","0.01760000":"0.61586819","0.01757536":"0.16689035","0.01756318":"0.16712246","0.01755000":"0.17880571","0.01753777":"0.02000000","0.01753000":"20.00000000","0.01752940":"6.33639183","0.01752200":"0.16744844","0.01751409":"0.16746315","0.01751000":"13.70645346","0.01750705":"0.16737687","0.01750703":"0.16745370","0.01750156":"0.87070000","0.01750030":"0.16745114","0.01750001":"1.20509760","0.01750000":"4.70871739","0.01749100":"0.30000000","0.01748000":"5.72082380","0.01744199":"2.27821626","0.01742307":"0.15241720","0.01742099":"0.21000000","0.01740156":"0.87080000","0.01740000":"2.51646494","0.01739764":"0.17243775","0.01738698":"5.47367120","0.01736889":"0.01000000","0.01735751":"0.16700000","0.01735430":"0.01063611","0.01735119":"0.57632935","0.01732467":"0.87010000","0.01730156":"0.87090000","0.01730000":"0.75144510","0.01725961":"0.01912268","0.01722467":"0.87020000","0.01720156":"0.87100000","0.01718725":"0.03300000","0.01717170":"1.74706058","0.01716824":"0.15384920","0.01712467":"0.87030000","0.01711000":"0.05844536","0.01710156":"0.87110000","0.01710000":"2.76023393","0.01708000":"0.08430913","0.01707173":"0.11715275","0.01704968":"0.17595696","0.01702467":"0.87040000","0.01700333":"0.09900000","0.01700156":"0.87120000","0.01700001":"0.84698186","0.01700000":"39.04824119","0.01697110":"0.01051466","0.01695900":"0.11793149","0.01694674":"0.08487382","0.01694673":"0.04428083","0.01693337":"0.05000000","0.01693288":"0.39392944","0.01692467":"0.87050000","0.01692117":"0.16792370","0.01690420":"7.00000000","0.01690156":"0.87130000","0.01690000":"0.61952663","0.01688000":"5.00000000","0.01687541":"0.52812621","0.01686000":"2.61923132","0.01682467":"0.87060000","0.01680156":"0.87140000","0.01680000":"0.36176667","0.01672467":"0.87070000","0.01670868":"0.17954799","0.01670156":"0.87150000","0.01670000":"13.25218564","0.01662467":"0.87080000","0.01660156":"0.87160000","0.01660000":"0.10000000","0.01659400":"2.00000000","0.01658287":"0.87010000","0.01657500":"0.00604224","0.01655555":"0.31227177","0.01655000":"1.51057402","0.01654675":"0.13985949","0.01653987":"1.86424470","0.01652467":"0.87090000","0.01650156":"0.87170000","0.01650001":"0.00612121","0.01650000":"3.39627334","0.01649003":"0.69724391","0.01648287":"0.87020000","0.01647890":"0.00859464","0.01646007":"1.00000000","0.01643000":"0.12175500","0.01642467":"0.87100000","0.01641927":"0.18517070","0.01641784":"9.36286518","0.01640156":"0.87180000","0.01640000":"0.60000000","0.01639296":"0.02653151","0.01638287":"0.87030000","0.01637499":"0.30534370","0.01632968":"0.00630754","0.01632467":"0.87110000","0.01631459":"1.83884486","0.01630156":"0.87190000","0.01630000":"0.10674847","0.01628287":"0.87040000","0.01628120":"0.00874124","0.01625900":"15.37609940","0.01624594":"0.16582851","0.01622467":"0.87120000","0.01620499":"0.00647948","0.01620156":"0.87200000","0.01620000":"0.10000000","0.01618287":"0.87050000","0.01616160":"1.85625186","0.01615651":"0.55727451","0.01612467":"0.87130000","0.01612450":"0.00850627","0.01612345":"1.50000032","0.01611000":"0.06207325","0.01610000":"1.07950311","0.01608287":"0.87060000","0.01608000":"0.01243782","0.01602467":"0.87140000","0.01601000":"2.00000000","0.01600000":"69.73844821","0.01598287":"0.87070000","0.01595001":"0.03080029","0.01592467":"0.87150000","0.01591737":"0.20438010","0.01590000":"1.81005031","0.01588287":"0.87080000","0.01588000":"1.00000000","0.01587458":"0.87010000","0.01582467":"0.87160000","0.01582016":"0.98059695","0.01578287":"0.87090000","0.01577458":"0.87020000","0.01575000":"0.12700600","0.01572467":"0.87170000","0.01571705":"1.12513862","0.01570000":"1.12791530","0.01568287":"0.87100000","0.01567557":"0.03135260","0.01567458":"0.87030000","0.01562467":"0.87180000","0.01560120":"16.25228252","0.01560000":"0.20000000","0.01558287":"0.87110000","0.01557821":"0.01557821","0.01557458":"0.87040000","0.01555682":"0.17905073","0.01555555":"0.32143777","0.01552467":"0.87190000","0.01552000":"0.21477663","0.01551000":"2.00000000","0.01550000":"1.66516129","0.01548287":"0.87120000","0.01547458":"0.87050000","0.01544444":"0.32374177","0.01543000":"0.05000000","0.01542956":"6.00000000","0.01542467":"0.87200000","0.01541547":"0.22580680","0.01541469":"0.01225747","0.01538287":"0.87130000","0.01537844":"13.00616577","0.01537458":"0.87060000","0.01534000":"0.13034800","0.01533333":"0.32608777","0.01533000":"0.21743857","0.01530610":"0.01000000","0.01528287":"0.87140000","0.01527752":"1.73115860","0.01527458":"0.87070000","0.01523000":"1.03717203","0.01522222":"0.32846777","0.01520000":"263.72697170","0.01519944":"1.00000000","0.01519827":"0.87010000","0.01518434":"0.29999987","0.01518287":"0.87150000","0.01517458":"0.87080000","0.01516006":"0.05000000","0.01511111":"0.33088777","0.01510034":"0.86321566","0.01510000":"1.70000000","0.01509827":"0.87020000","0.01509000":"2.00000000","0.01508287":"0.87160000","0.01507458":"0.87090000","0.01502916":"1.08381762","0.01501000":"7.00000000","0.01500001":"0.00673333","0.01500000":"58.56291868","0.01499999":"0.33333777","0.01499827":"0.87030000","0.01498287":"0.87170000","0.01497458":"0.87100000","0.01493417":"0.03452351","0.01489827":"0.87040000","0.01488888":"0.33834777","0.01488287":"0.87180000","0.01488166":"0.05000000","0.01487458":"0.87110000","0.01482262":"0.05000000","0.01481000":"17.72435787","0.01480927":"0.10000000","0.01479827":"0.87050000","0.01478424":"0.03381980","0.01478287":"0.87190000","0.01477777":"0.33834777","0.01477458":"0.87120000","0.01472112":"0.05000000","0.01469827":"0.87060000","0.01468287":"0.87200000","0.01467458":"0.87130000","0.01466666":"0.34091777","0.01462006":"0.05000000","0.01459930":"1.95654792","0.01459827":"0.87070000","0.01457458":"0.87140000","0.01455555":"0.34351777","0.01455250":"0.87010000","0.01452668":"0.03333300","0.01452068":"0.03333300","0.01451200":"68.90848953","0.01450001":"0.05000000","0.01450000":"0.82500000","0.01449827":"0.87080000","0.01447458":"0.87150000","0.01445250":"0.87020000","0.01444444":"0.37293934","0.01441945":"0.05000000","0.01439827":"0.87090000","0.01437458":"0.87160000","0.01435250":"0.87030000","0.01431000":"0.13972000","0.01429827":"0.87100000","0.01427770":"1.19790356","0.01427458":"0.87170000","0.01425250":"0.87040000","0.01423759":"0.30000000","0.01422222":"0.38468537","0.01420000":"0.05000000","0.01419827":"0.87110000","0.01417458":"0.87180000","0.01415250":"0.87050000","0.01409827":"0.87120000","0.01407458":"0.87190000","0.01405902":"0.00711999","0.01405250":"0.87060000","0.01400641":"0.05000000","0.01400000":"53.05114286","0.01399999":"0.39405242","0.01399827":"0.87130000","0.01397458":"0.87200000","0.01395250":"0.87070000","0.01395000":"0.14339700","0.01394554":"0.00788783","0.01389827":"0.87140000","0.01385250":"0.87080000","0.01382084":"0.10000000","0.01379827":"0.87150000","0.01378040":"0.10000000","0.01377777":"0.41996779","0.01375250":"0.87090000","0.01370007":"13.00000000","0.01369827":"0.87160000","0.01366666":"0.36548777","0.01366600":"1.50000000","0.01365250":"0.87100000","0.01359827":"0.87170000","0.01355555":"0.36848777","0.01355250":"0.87110000","0.01350001":"0.04060737","0.01349827":"0.87180000","0.01349000":"0.24709661","0.01345250":"0.87120000","0.01339827":"0.87190000","0.01335250":"0.87130000","0.01333333":"0.38644955","0.01329827":"0.87200000","0.01328424":"1.05388039","0.01325250":"0.87140000","0.01325069":"0.10000000","0.01323759":"0.30000000","0.01323188":"0.05000000","0.01322222":"0.37777213","0.01315562":"3.80065710","0.01315250":"0.87150000","0.01305250":"0.87160000","0.01303000":"0.23400000","0.01300000":"50.93724077","0.01299999":"0.43083695","0.01296794":"0.10000000","0.01295250":"0.87170000","0.01288888":"0.38754777","0.01286007":"0.05000000","0.01285250":"0.87180000","0.01277777":"0.39091220","0.01277766":"0.01277766","0.01275250":"0.87190000","0.01270000":"2.50000000","0.01266666":"0.21710528","0.01265250":"0.87200000","0.01264009":"0.03333300","0.01260000":"0.80000000","0.01259000":"6.95316686","0.01255555":"0.40400221","0.01254926":"0.16697325","0.01251212":"0.49999997","0.01250000":"0.88000000","0.01244444":"0.40413777","0.01239142":"0.10000000","0.01234567":"0.76430224","0.01233333":"1.71320564","0.01230000":"25.56692520","0.01228705":"0.01561809","0.01223759":"0.93539006","0.01222222":"0.42222996","0.01215114":"0.15185528","0.01215112":"0.28745677","0.01215110":"1.52650418","0.01211111":"0.42449777","0.01206788":"0.05000000","0.01201989":"0.05000000","0.01200000":"53.38662694","0.01199999":"0.43026777","0.01190000":"2.50000000","0.01188888":"0.42322777","0.01180011":"6.37080634","0.01177777":"0.43477577","0.01172008":"0.15000000","0.01172006":"0.05000000","0.01168471":"0.03333300","0.01168089":"0.10000000","0.01167305":"0.10000000","0.01166666":"0.43657577","0.01164886":"0.05000000","0.01164000":"0.05000000","0.01162499":"0.08284588","0.01160000":"2.17241466","0.01158489":"0.03333300","0.01156086":"0.03333300","0.01156067":"0.10000000","0.01155555":"0.47279977","0.01152000":"2.50000000","0.01144444":"0.44664777","0.01133333":"0.45762797","0.01130602":"0.05000000","0.01124698":"1.00000000","0.01122222":"0.46234777","0.01122000":"1.78253120","0.01111111":"0.46716777","0.01104554":"0.00995878","0.01100000":"7.07893074","0.01099999":"0.95083037","0.01089000":"0.30609122","0.01077777":"0.46392777","0.01072406":"0.10000000","0.01072000":"5.00000000","0.01066666":"0.46875777","0.01065500":"0.31284217","0.01055555":"0.47368777","0.01044444":"0.47872777","0.01040151":"6.02529465","0.01035000":"96.61835749","0.01034667":"0.32216495","0.01033333":"0.48387177","0.01033000":"2.10000000","0.01032398":"2.84523120","0.01029405":"0.10000000","0.01026000":"0.26500000","0.01022222":"1.00000000","0.01022000":"0.32615786","0.01012375":"1.00000000","0.01011111":"0.49450777","0.01006118":"0.10000000","0.01002967":"0.18002967","0.01000778":"9.47280346","0.01000679":"0.10000000","0.01000001":"0.01099999","0.01000000":"57.22867792","0.00999999":"0.50000777","0.00977777":"0.51136777","0.00955555":"0.52325777","0.00950653":"0.34612572","0.00940000":"0.11740363","0.00935965":"0.10000000","0.00919000":"0.36271309","0.00911111":"1.00000000","0.00904534":"0.01216096","0.00900000":"6.66746222","0.00899999":"0.55555777","0.00899333":"0.37064492","0.00896061":"0.10000000","0.00896000":"0.37202381","0.00895590":"0.02050574","0.00895383":"5.58420252","0.00880000":"0.10000000","0.00877777":"0.56962777","0.00860000":"7.80000000","0.00855999":"199.99919929","0.00842000":"2.50000000","0.00833333":"1.93200078","0.00830397":"0.10392986","0.00811111":"1.00000000","0.00811000":"1.50000000","0.00808000":"2.77624753","0.00800121":"0.62490548","0.00800000":"254.62500124","0.00799999":"0.62500777","0.00777000":"1.00000000","0.00776000":"0.42955326","0.00770000":"1.29870130","0.00766500":"0.43487715","0.00757568":"1.00000000","0.00755555":"0.66176777","0.00755005":"0.09433712","0.00750000":"10.00000000","0.00747111":"8.38860124","0.00744094":"0.09312933","0.00735028":"482.32137279","0.00730000":"0.20225479","0.00726000":"0.45913682","0.00724000":"0.08813430","0.00723000":"0.64000002","0.00722222":"0.69231777","0.00711111":"1.00000000","0.00710333":"0.46926326","0.00704554":"0.01561272","0.00701111":"1.00000000","0.00690000":"23.00000000","0.00674798":"0.90687198","0.00674500":"0.49419323","0.00638390":"0.09666600","0.00625000":"1.76000000","0.00621609":"0.17306286","0.00620000":"20.00000000","0.00612667":"0.54406964","0.00611111":"1.00000000","0.00609034":"0.88303280","0.00601111":"1.00000000","0.00600000":"0.20678446","0.00597333":"0.55803571","0.00580000":"0.11562725","0.00567337":"0.17626208","0.00564222":"0.05176866","0.00555555":"2.00000000","0.00547603":"1.00000000","0.00544500":"0.61218243","0.00539000":"1.00000000","0.00532750":"0.62568434","0.00517941":"0.10000000","0.00517333":"0.64432990","0.00514683":"0.06397259","0.00514534":"0.02137857","0.00511280":"18.54203822","0.00511000":"0.65231572","0.00501111":"2.00000000","0.00500100":"1.10000000","0.00500000":"0.60545120","0.00480000":"5.00000000","0.00459500":"0.72542619","0.00450000":"2.04440667","0.00449667":"0.74128984","0.00448000":"0.74404762","0.00444444":"3.00000000","0.00420000":"15.00000000","0.00401111":"2.00000000","0.00401000":"3.00000000","0.00400121":"0.50201813","0.00388000":"0.85910653","0.00383250":"0.86975429","0.00364200":"51.24272652","0.00363000":"0.91827365","0.00360408":"0.90000000","0.00356765":"0.09000000","0.00355167":"0.93852651","0.00351100":"6.19640231","0.00347837":"12.93709410","0.00347508":"0.10000000","0.00337250":"0.98838646","0.00333624":"1.00000000","0.00333333":"5.91332718","0.00320000":"3.12500000","0.00319327":"0.07254321","0.00306333":"1.08813928","0.00301111":"2.00000000","0.00301000":"3.00000000","0.00300126":"0.66638678","0.00300000":"3.90000000","0.00298667":"1.11607143","0.00282781":"1.00000000","0.00281180":"0.03559998","0.00276383":"0.99000000","0.00272250":"1.22436486","0.00271133":"0.04635562","0.00270000":"1.00000000","0.00267500":"1.00000000","0.00267120":"374.36358192","0.00266375":"1.25136868","0.00259554":"0.19264199","0.00251436":"0.07671579","0.00251285":"1.00000000","0.00250000":"0.04455052","0.00233333":"2.14286777","0.00229750":"1.45085238","0.00224672":"1.00000008","0.00224000":"1.48809524","0.00222222":"5.00000000","0.00220271":"18.66289707","0.00220000":"0.10000000","0.00217300":"0.06250000","0.00201116":"2.48612740","0.00201111":"6.97237843","0.00200100":"15.00000000","0.00200000":"54.78000000","0.00190428":"0.99000000","0.00186231":"1.00000000","0.00180000":"1.00000000","0.00175996":"0.14457147","0.00160000":"6.25000000","0.00156666":"3.23396270","0.00150001":"133.06577956","0.00150000":"1.13333334","0.00148993":"10.00000000","0.00139070":"0.24041795","0.00138451":"1.00000000","0.00138000":"0.45000000","0.00137002":"4.00382284","0.00133333":"0.30000000","0.00131136":"1.00000000","0.00130842":"1.00000000","0.00129777":"0.38528398","0.00124333":"0.10000000","0.00123333":"0.09000000","0.00122333":"0.08901089","0.00118304":"82.03793208","0.00112233":"10.00000000","0.00111000":"21.63841180","0.00110000":"1.00000000","0.00102000":"1.00000000","0.00101111":"10.00000000","0.00100110":"150.00000000","0.00100100":"10.00000000","0.00100000":"181.20000000","0.00090111":"5.00000000","0.00087300":"12.00000000","0.00084828":"1.00000000","0.00080111":"5.00000000","0.00080000":"12.50000000","0.00079301":"10.00000000","0.00077999":"1.00000000","0.00073726":"1.00000000","0.00070111":"5.00000000","0.00070000":"1.00000000","0.00064889":"0.77056203","0.00060111":"5.00000000","0.00060000":"4.16837134","0.00052451":"1.00000000","0.00050130":"3.34127269","0.00050111":"5.00000000","0.00050000":"3.00000000","0.00046193":"2164.78705892","0.00045663":"1.00000000","0.00043148":"1.94319078","0.00042000":"10.00000000","0.00040111":"5.00000000","0.00040100":"8.60995012","0.00040000":"27.50016378","0.00036041":"0.90000000","0.00033235":"100.00000000","0.00033234":"451.33550174","0.00032472":"1.00000000","0.00032444":"1.54111700","0.00032266":"6.19850617","0.00030111":"5.00000000","0.00030000":"5.00000000","0.00029763":"1.00000000","0.00024000":"1.00000000","0.00023500":"1.00306383","0.00020618":"150.00000000","0.00020111":"5.00000000","0.00020010":"40.00000000","0.00020000":"1055.00000000","0.00017000":"1.00000000","0.00016222":"3.08223400","0.00015883":"100.00000000","0.00015882":"1259.28724342","0.00015054":"1.00000000","0.00013334":"1.00000000","0.00011540":"1.00000000","0.00011164":"1.00000000","0.00011111":"89.09189092","0.00010650":"9.00000000","0.00010510":"2.00000000","0.00010359":"1.00000000","0.00010111":"10.00000000","0.00010000":"2052.10260000","0.00009726":"17.85554185","0.00009170":"10.00000000","0.00008800":"8.00000000","0.00008000":"2.02050000","0.00007186":"6.95811300","0.00006060":"130.00000000","0.00005126":"1070.00000000","0.00005120":"195.31250000","0.00005000":"2120.00000000","0.00004295":"202.34435389","0.00004168":"95.96928983","0.00004000":"200.00000000","0.00003638":"137.43815283","0.00003500":"114.28657143","0.00003492":"6.90074951","0.00003101":"500.00000000","0.00003100":"1000.00000000","0.00002560":"390.62500000","0.00002500":"20000.00000000","0.00002000":"55.00000000","0.00001280":"781.25000000","0.00001010":"50.00000000","0.00001005":"146.26965174","0.00001000":"12109.99999999","0.00000640":"1562.50000000","0.00000550":"800.00000000","0.00000500":"200.00000000","0.00000331":"1000.00000000","0.00000330":"11479.02727273","0.00000320":"3125.00000000","0.00000200":"1000.00000001","0.00000178":"65.00000000","0.00000170":"100.00000000","0.00000164":"210.17073171","0.00000160":"6250.00000000","0.00000100":"1999.00000000","0.00000095":"1612.31578947","0.00000090":"1111.11111111","0.00000080":"12500.00000000","0.00000054":"557.96296296","0.00000040":"25000.00000000","0.00000020":"50000.00000000","0.00000010":"200000.00000000","0.00000005":"200000.00000000","0.00000004":"2500.00000000","0.00000002":"556100.00000000","0.00000001":"1182263.00000000"}]}]]]`) + err = p.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } + + pressXToJSON = []byte(`[148,827984670,[["o",0,"0.02328500","0.00000000"],["o",0,"0.02328498","0.04303557"]]]`) + err = p.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsHandleAccountData(t *testing.T) { + t.Parallel() + err := p.getCurrencyIDMap() + if err != nil { + t.Error(err) + } + jsons := []string{ + `[1000,"",[["o",807230187,"0.00000000", "f"],["b",267,"e","0.10000000"]]]`, + `[1000,"",[["n",50,807230187,0,"1000.00000000","0.10000000","2018-11-07 16:42:42"],["b",267,"e","-0.10000000"]]]`, + `[1000,"",[["t", 12345, "0.03000000", "0.50000000", "0.00250000", 0, 6083059, "0.00000375", "2018-09-08 05:54:09", "12345"]]]`, + `[1000,"",[["k", 1337, ""]]]`, + } + for i := range jsons { + err := p.wsHandleData([]byte(jsons[i])) + if err != nil { + t.Error(err) + } + } +} diff --git a/exchanges/poloniex/poloniex_types.go b/exchanges/poloniex/poloniex_types.go index e296ef16..f2becbb0 100644 --- a/exchanges/poloniex/poloniex_types.go +++ b/exchanges/poloniex/poloniex_types.go @@ -8,14 +8,14 @@ import ( // Ticker holds ticker data type Ticker struct { - ID int `json:"id"` + ID float64 `json:"id"` Last float64 `json:"last,string"` LowestAsk float64 `json:"lowestAsk,string"` HighestBid float64 `json:"highestBid,string"` PercentChange float64 `json:"percentChange,string"` BaseVolume float64 `json:"baseVolume,string"` QuoteVolume float64 `json:"quoteVolume,string"` - IsFrozen int `json:"isFrozen,string"` + IsFrozen int64 `json:"isFrozen,string"` High24Hr float64 `json:"high24hr,string"` Low24Hr float64 `json:"low24hr,string"` } @@ -68,7 +68,7 @@ type TradeHistory struct { // ChartData holds kline data type ChartData struct { - Date int `json:"date"` + Date int64 `json:"date"` High float64 `json:"high"` Low float64 `json:"low"` Open float64 `json:"open"` @@ -81,23 +81,23 @@ type ChartData struct { // Currencies contains currency information type Currencies struct { - ID int `json:"id"` + ID float64 `json:"id"` Name string `json:"name"` MaxDailyWithdrawal string `json:"maxDailyWithdrawal"` TxFee float64 `json:"txFee,string"` - MinConfirmations int `json:"minConf"` + MinConfirmations int64 `json:"minConf"` DepositAddresses interface{} `json:"depositAddress"` - Disabled int `json:"disabled"` - Delisted int `json:"delisted"` - Frozen int `json:"frozen"` + Disabled int64 `json:"disabled"` + Delisted int64 `json:"delisted"` + Frozen int64 `json:"frozen"` } // LoanOrder holds loan order information type LoanOrder struct { Rate float64 `json:"rate,string"` Amount float64 `json:"amount,string"` - RangeMin int `json:"rangeMin"` - RangeMax int `json:"rangeMax"` + RangeMin int64 `json:"rangeMin"` + RangeMax int64 `json:"rangeMax"` } // LoanOrders holds loan order information range @@ -129,7 +129,7 @@ type DepositsWithdrawals struct { Currency string `json:"currency"` Address string `json:"address"` Amount float64 `json:"amount,string"` - Confirmations int `json:"confirmations"` + Confirmations int64 `json:"confirmations"` TransactionID string `json:"txid"` Timestamp int64 `json:"timestamp"` Status string `json:"status"` @@ -139,7 +139,7 @@ type DepositsWithdrawals struct { Currency string `json:"currency"` Address string `json:"address"` Amount float64 `json:"amount,string"` - Confirmations int `json:"confirmations"` + Confirmations int64 `json:"confirmations"` TransactionID string `json:"txid"` Timestamp int64 `json:"timestamp"` Status string `json:"status"` @@ -210,13 +210,13 @@ type OrderResponse struct { // GenericResponse is a response type for exchange generic responses type GenericResponse struct { - Success int `json:"success"` + Success int64 `json:"success"` Error string `json:"error"` } // MoveOrderResponse is a response type for move order trades type MoveOrderResponse struct { - Success int `json:"success"` + Success int64 `json:"success"` Error string `json:"error"` OrderNumber int64 `json:"orderNumber,string"` Trades map[string][]ResultingTrades `json:"resultingTrades"` @@ -247,13 +247,13 @@ type Margin struct { // MarginPosition holds margin positional information type MarginPosition struct { - Amount float64 `json:"amount,string"` - Total float64 `json:"total,string"` - BasePrice float64 `json:"basePrice,string"` - LiquidiationPrice float64 `json:"liquidiationPrice"` - ProfitLoss float64 `json:"pl,string"` - LendingFees float64 `json:"lendingFees,string"` - Type string `json:"type"` + Amount float64 `json:"amount,string"` + Total float64 `json:"total,string"` + BasePrice float64 `json:"basePrice,string"` + LiquidationPrice float64 `json:"liquidationPrice"` + ProfitLoss float64 `json:"pl,string"` + LendingFees float64 `json:"lendingFees,string"` + Type string `json:"type"` } // LoanOffer holds loan offer information @@ -261,7 +261,7 @@ type LoanOffer struct { ID int64 `json:"id"` Rate float64 `json:"rate,string"` Amount float64 `json:"amount,string"` - Duration int `json:"duration"` + Duration int64 `json:"duration"` AutoRenew bool `json:"autoRenew"` Date string `json:"date"` } @@ -414,16 +414,6 @@ type WsAccountBalanceUpdateResponse struct { amount float64 } -// WsNewLimitOrderResponse Authenticated Ws Account data -type WsNewLimitOrderResponse struct { - currencyID float64 - orderNumber float64 - orderType float64 - rate float64 - amount float64 - date time.Time -} - // WsOrderUpdateResponse Authenticated Ws Account data type WsOrderUpdateResponse struct { OrderNumber float64 diff --git a/exchanges/poloniex/poloniex_websocket.go b/exchanges/poloniex/poloniex_websocket.go index 84fb584e..d671f71f 100644 --- a/exchanges/poloniex/poloniex_websocket.go +++ b/exchanges/poloniex/poloniex_websocket.go @@ -10,6 +10,7 @@ import ( "time" "github.com/gorilla/websocket" + "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/common/crypto" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" @@ -19,7 +20,6 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wsorderbook" - "github.com/thrasher-corp/gocryptotrader/log" ) const ( @@ -33,7 +33,7 @@ const ( var ( // currencyIDMap stores a map of currencies associated with their ID - currencyIDMap map[int]string + currencyIDMap map[float64]string ) // WsConnect initiates a websocket connection @@ -47,8 +47,20 @@ func (p *Poloniex) WsConnect() error { return err } + err2 := p.getCurrencyIDMap() + if err2 != nil { + return err2 + } + + go p.wsReadData() + p.GenerateDefaultSubscriptions() + + return nil +} + +func (p *Poloniex) getCurrencyIDMap() error { if currencyIDMap == nil { - currencyIDMap = make(map[int]string) + currencyIDMap = make(map[float64]string) resp, err := p.GetTicker() if err != nil { return err @@ -58,10 +70,6 @@ func (p *Poloniex) WsConnect() error { currencyIDMap[v.ID] = k } } - - go p.WsHandleData() - p.GenerateDefaultSubscriptions() - return nil } @@ -75,8 +83,8 @@ func checkSubscriptionSuccess(data []interface{}) bool { return data[1].(float64) == 1 } -// WsHandleData handles data from the websocket connection -func (p *Poloniex) WsHandleData() { +// wsReadData handles data from the websocket connection +func (p *Poloniex) wsReadData() { p.Websocket.Wg.Add(1) defer func() { @@ -87,7 +95,6 @@ func (p *Poloniex) WsHandleData() { select { case <-p.Websocket.ShutdownC: return - default: resp, err := p.WebsocketConn.ReadMessage() if err != nil { @@ -95,188 +102,319 @@ func (p *Poloniex) WsHandleData() { return } p.Websocket.TrafficAlert <- struct{}{} - var result interface{} - err = json.Unmarshal(resp.Raw, &result) + err = p.wsHandleData(resp.Raw) if err != nil { p.Websocket.DataHandler <- err - continue - } - switch data := result.(type) { - case map[string]interface{}: - // subscription error - p.Websocket.DataHandler <- errors.New(data["error"].(string)) - case []interface{}: - chanID := int(data[0].(float64)) - if len(data) == 2 && chanID != wsHeartbeat { - if checkSubscriptionSuccess(data) { - if p.Verbose { - log.Debugf(log.ExchangeSys, - "%s websocket subscribed to channel successfully. %d", - p.Name, - chanID) - } - } else { - p.Websocket.DataHandler <- fmt.Errorf("%s websocket subscription to channel failed. %d", - p.Name, - chanID) - } - continue - } - - switch chanID { - case wsAccountNotificationID: - p.wsHandleAccountData(data[2].([][]interface{})) - case wsTickerDataID: - p.wsHandleTickerData(data) - case ws24HourExchangeVolumeID: - case wsHeartbeat: - default: - if len(data) > 2 { - subData := data[2].([]interface{}) - - for x := range subData { - dataL2 := subData[x] - dataL3 := dataL2.([]interface{}) - - switch getWSDataType(dataL2) { - case "i": - dataL3map := dataL3[1].(map[string]interface{}) - currencyPair, ok := dataL3map["currencyPair"].(string) - if !ok { - 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 <- fmt.Errorf("%s websocket could not find orderbook data in map", - p.Name) - continue - } - - err = p.WsProcessOrderbookSnapshot(orderbookData, - currencyPair) - if err != nil { - p.Websocket.DataHandler <- err - continue - } - - p.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{ - Exchange: p.Name, - Asset: asset.Spot, - Pair: currency.NewPairFromString(currencyPair), - } - case "o": - currencyPair := currencyIDMap[chanID] - err = p.WsProcessOrderbookUpdate(int64(data[1].(float64)), - dataL3, - currencyPair) - if err != nil { - p.Websocket.DataHandler <- err - continue - } - - p.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{ - Exchange: p.Name, - Asset: asset.Spot, - Pair: currency.NewPairFromString(currencyPair), - } - case "t": - currencyPair := currencyIDMap[chanID] - var trade WsTrade - trade.Symbol = currencyIDMap[chanID] - trade.TradeID, _ = strconv.ParseInt(dataL3[1].(string), 10, 64) - // 1 for buy 0 for sell - side := order.Buy - if dataL3[2].(float64) != 1 { - side = order.Sell - } - trade.Side = side.Lower() - 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{ - Timestamp: time.Unix(trade.Timestamp, 0), - CurrencyPair: currency.NewPairFromString(currencyPair), - Side: trade.Side, - Amount: trade.Volume, - Price: trade.Price, - } - } - } - } - } } } } } -func (p *Poloniex) wsHandleTickerData(data []interface{}) { +func (p *Poloniex) wsHandleData(respRaw []byte) error { + var result interface{} + err := json.Unmarshal(respRaw, &result) + if err != nil { + return err + } + if data, ok := result.([]interface{}); ok { + if len(data) == 0 { + return nil + } + if len(data) == 1 { + // heartbeat + return nil + } + if len(data) == 2 { + // subscription acknowledgement / heartbeat + return nil + } + if channelID, ok := data[0].(float64); ok { + switch channelID { + case ws24HourExchangeVolumeID: + return nil + case wsAccountNotificationID: + if notificationsArray, ok := data[2].([]interface{}); ok { + if _, ok := notificationsArray[0].([]interface{}); ok { + for i := 0; i < len(notificationsArray); i++ { + if notification, ok := (notificationsArray[i]).([]interface{}); ok { + switch notification[0].(string) { + case "o": + var amount float64 + amount, err = strconv.ParseFloat(notification[2].(string), 64) + if err != nil { + return err + } + var oStatus order.Status + var oType = notification[3].(string) + switch { + case amount > 0 && (oType == "f" || oType == "s"): + oStatus = order.PartiallyFilled + case amount == 0 && (oType == "f" || oType == "s"): + oStatus = order.Filled + case amount > 0 && oType == "c": + oStatus = order.PartiallyCancelled + case amount == 0 && oType == "c": + oStatus = order.Cancelled + } + response := &order.Modify{ + RemainingAmount: amount, + Exchange: p.Name, + ID: strconv.FormatFloat(notification[1].(float64), 'f', -1, 64), + Type: order.Limit, + Status: oStatus, + AssetType: asset.Spot, + } + p.Websocket.DataHandler <- response + case "n": + var timeParse time.Time + timeParse, err = time.Parse(common.SimpleTimeFormat, notification[6].(string)) + if err != nil { + return err + } + var rate, amount float64 + rate, err = strconv.ParseFloat(notification[4].(string), 64) + if err != nil { + return err + } + amount, err = strconv.ParseFloat(notification[5].(string), 64) + if err != nil { + return err + } + var buySell order.Side + switch notification[2].(float64) { + case 0: + buySell = order.Buy + case 1: + buySell = order.Sell + } + var currPair currency.Pair + if currPairFromMap, ok := currencyIDMap[notification[1].(float64)]; ok { + currPair = currency.NewPairFromString(currPairFromMap) + } else { + // It is better to still log an order which you can recheck later, rather than error out + p.Websocket.DataHandler <- fmt.Errorf(p.Name+ + " - Unknown currency pair ID. "+ + "Currency will appear as the pair ID: '%v'", + notification[1].(float64)) + currPair = currency.NewPairFromString(strconv.FormatFloat(notification[1].(float64), 'f', -1, 64)) + } + var a asset.Item + a, err = p.GetPairAssetType(currPair) + if err != nil { + return err + } + response := &order.Detail{ + Price: rate, + Amount: amount, + Exchange: p.Name, + ID: strconv.FormatFloat(notification[2].(float64), 'f', -1, 64), + Type: order.Limit, + Side: buySell, + Status: order.New, + AssetType: a, + Date: timeParse, + Pair: currPair, + } + p.Websocket.DataHandler <- response + case "b": + var amount float64 + amount, err = strconv.ParseFloat(notification[3].(string), 64) + if err != nil { + return err + } + + response := WsAccountBalanceUpdateResponse{ + currencyID: notification[1].(float64), + wallet: notification[2].(string), + amount: amount, + } + p.Websocket.DataHandler <- response + case "t": + var timeParse time.Time + timeParse, err = time.Parse(common.SimpleTimeFormat, notification[8].(string)) + if err != nil { + return err + } + var rate, amount, totalFee float64 + rate, err = strconv.ParseFloat(notification[2].(string), 64) + if err != nil { + return err + } + amount, err = strconv.ParseFloat(notification[3].(string), 64) + if err != nil { + return err + } + totalFee, err = strconv.ParseFloat(notification[7].(string), 64) + if err != nil { + return err + } + var trades []order.TradeHistory + trades = append(trades, order.TradeHistory{ + Price: rate, + Amount: amount, + Fee: totalFee, + Exchange: p.Name, + TID: strconv.FormatFloat(notification[1].(float64), 'f', -1, 64), + Timestamp: timeParse, + }) + response := &order.Modify{ + ID: strconv.FormatFloat(notification[6].(float64), 'f', -1, 64), + Fee: totalFee, + Trades: trades, + } + p.Websocket.DataHandler <- response + case "k": + response := &order.Modify{ + Exchange: p.Name, + ID: strconv.FormatFloat(notification[1].(float64), 'f', -1, 64), + Status: order.Cancelled, + } + p.Websocket.DataHandler <- response + } + } + } + } + } + case wsTickerDataID: + return p.wsHandleTickerData(data) + default: + subData := data[2].([]interface{}) + for x := range subData { + dataL2 := subData[x] + + switch getWSDataType(dataL2) { + case "i": + dataL3 := dataL2.([]interface{}) + dataL3map := dataL3[1].(map[string]interface{}) + currencyPair, ok := dataL3map["currencyPair"].(string) + if !ok { + return fmt.Errorf("%s websocket could not find currency pair in map", + p.Name) + } + + orderbookData, ok := dataL3map["orderBook"].([]interface{}) + if !ok { + return fmt.Errorf("%s websocket could not find orderbook data in map", + p.Name) + } + + err = p.WsProcessOrderbookSnapshot(orderbookData, + currencyPair) + if err != nil { + return err + } + + p.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{ + Exchange: p.Name, + Asset: asset.Spot, + Pair: currency.NewPairFromString(currencyPair), + } + case "o": + currencyPair := currencyIDMap[channelID] + dataL3 := dataL2.([]interface{}) + err = p.WsProcessOrderbookUpdate(int64(data[1].(float64)), + dataL3, + currencyPair) + if err != nil { + return err + } + + p.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{ + Exchange: p.Name, + Asset: asset.Spot, + Pair: currency.NewPairFromString(currencyPair), + } + case "t": + currencyPair := currencyIDMap[channelID] + var trade WsTrade + trade.Symbol = currencyIDMap[channelID] + dataL3 := dataL2.([]interface{}) + trade.TradeID, err = strconv.ParseInt(dataL3[1].(string), 10, 64) + if err != nil { + return err + } + side := order.Buy + if dataL3[2].(float64) != 1 { + side = order.Sell + } + trade.Volume, err = strconv.ParseFloat(dataL3[3].(string), 64) + if err != nil { + return err + } + trade.Price, err = strconv.ParseFloat(dataL3[4].(string), 64) + if err != nil { + return err + } + trade.Timestamp = int64(dataL3[5].(float64)) + + p.Websocket.DataHandler <- wshandler.TradeData{ + Timestamp: time.Unix(trade.Timestamp, 0), + CurrencyPair: currency.NewPairFromString(currencyPair), + Side: side, + Amount: trade.Volume, + Price: trade.Price, + } + default: + p.Websocket.DataHandler <- wshandler.UnhandledMessageWarning{Message: p.Name + wshandler.UnhandledMessage + string(respRaw)} + return nil + } + } + } + } + } + return nil +} + +func (p *Poloniex) wsHandleTickerData(data []interface{}) error { tickerData := data[2].([]interface{}) var t WsTicker - currencyPair := currency.NewPairDelimiter(currencyIDMap[int(tickerData[0].(float64))], delimiterUnderscore) + currencyPair := currency.NewPairDelimiter(currencyIDMap[tickerData[0].(float64)], delimiterUnderscore) if !p.GetEnabledPairs(asset.Spot).Contains(currencyPair, true) { - return + // Ticker subscription receives all currencies, no specific subscription + // There should be no error associated with receiving data of disabled currency ticker data + return nil } var err error t.LastPrice, err = strconv.ParseFloat(tickerData[1].(string), 64) if err != nil { - p.Websocket.DataHandler <- err - return + return err } t.LowestAsk, err = strconv.ParseFloat(tickerData[2].(string), 64) if err != nil { - p.Websocket.DataHandler <- err - return + return err } t.HighestBid, err = strconv.ParseFloat(tickerData[3].(string), 64) if err != nil { - p.Websocket.DataHandler <- err - return + return err } t.PercentageChange, err = strconv.ParseFloat(tickerData[4].(string), 64) if err != nil { - p.Websocket.DataHandler <- err - return + return err } t.BaseCurrencyVolume24H, err = strconv.ParseFloat(tickerData[5].(string), 64) if err != nil { - p.Websocket.DataHandler <- err - return + return err } t.QuoteCurrencyVolume24H, err = strconv.ParseFloat(tickerData[6].(string), 64) if err != nil { - p.Websocket.DataHandler <- err - return + return err } t.IsFrozen = tickerData[7].(float64) == 1 t.HighestTradeIn24H, err = strconv.ParseFloat(tickerData[8].(string), 64) if err != nil { - p.Websocket.DataHandler <- err - return + return err } t.LowestTradePrice24H, err = strconv.ParseFloat(tickerData[9].(string), 64) if err != nil { - p.Websocket.DataHandler <- err - return + return err } p.Websocket.DataHandler <- &ticker.Price{ @@ -291,103 +429,7 @@ func (p *Poloniex) wsHandleTickerData(data []interface{}) { AssetType: asset.Spot, Pair: currencyPair, } -} - -// wsHandleAccountData Parses account data and sends to datahandler -func (p *Poloniex) wsHandleAccountData(accountData [][]interface{}) { - for i := range accountData { - switch accountData[i][0].(string) { - case "b": - 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), - amount: amount, - } - p.Websocket.DataHandler <- response - case "n": - 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), - orderNumber: accountData[i][2].(float64), - orderType: accountData[i][3].(float64), - rate: rate, - amount: amount, - date: timeParse, - } - p.Websocket.DataHandler <- response - case "o": - response := WsOrderUpdateResponse{ - OrderNumber: accountData[i][1].(float64), - NewAmount: accountData[i][2].(string), - } - p.Websocket.DataHandler <- response - case "t": - 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), - Rate: rate, - Amount: amount, - FeeMultiplier: feeMultiplier, - FundingType: accountData[i][5].(float64), - OrderNumber: accountData[i][6].(float64), - TotalFee: totalFee, - Date: timeParse, - } - p.Websocket.DataHandler <- response - } - } + return nil } // WsProcessOrderbookSnapshot processes a new orderbook snapshot into a local diff --git a/exchanges/poloniex/poloniex_wrapper.go b/exchanges/poloniex/poloniex_wrapper.go index 63e1acd9..b40361a3 100644 --- a/exchanges/poloniex/poloniex_wrapper.go +++ b/exchanges/poloniex/poloniex_wrapper.go @@ -101,6 +101,8 @@ func (p *Poloniex) SetDefaults() { Subscribe: true, Unsubscribe: true, AuthenticatedEndpoints: true, + GetOrders: true, + GetOrder: true, }, WithdrawPermissions: exchange.AutoWithdrawCryptoWithAPIPermission | exchange.NoFiatWithdrawals, @@ -379,8 +381,8 @@ func (p *Poloniex) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) { return submitOrderResponse, err } - fillOrKill := s.OrderType == order.Market - isBuyOrder := s.OrderSide == order.Buy + fillOrKill := s.Type == order.Market + isBuyOrder := s.Side == order.Buy response, err := p.PlaceOrder(s.Pair.String(), s.Price, s.Amount, @@ -395,7 +397,7 @@ func (p *Poloniex) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) { } submitOrderResponse.IsOrderPlaced = true - if s.OrderType == order.Market { + if s.Type == order.Market { submitOrderResponse.FullyMatched = true } return submitOrderResponse, nil @@ -404,7 +406,7 @@ func (p *Poloniex) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) { // ModifyOrder will allow of changing orderbook placement and limit to // market conversion func (p *Poloniex) ModifyOrder(action *order.Modify) (string, error) { - oID, err := strconv.ParseInt(action.OrderID, 10, 64) + oID, err := strconv.ParseInt(action.ID, 10, 64) if err != nil { return "", err } @@ -423,7 +425,7 @@ func (p *Poloniex) ModifyOrder(action *order.Modify) (string, error) { // CancelOrder cancels an order by its corresponding ID number func (p *Poloniex) CancelOrder(order *order.Cancel) error { - orderIDInt, err := strconv.ParseInt(order.OrderID, 10, 64) + orderIDInt, err := strconv.ParseInt(order.ID, 10, 64) if err != nil { return err } @@ -528,7 +530,7 @@ func (p *Poloniex) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, for i := range resp.Data[key] { orderSide := order.Side(strings.ToUpper(resp.Data[key][i].Type)) - orderDate, err := time.Parse(poloniexDateLayout, resp.Data[key][i].Date) + orderDate, err := time.Parse(common.SimpleTimeFormat, resp.Data[key][i].Date) if err != nil { log.Errorf(log.ExchangeSys, "Exchange %v Func %v Order %v Could not parse date to unix with value of %v", @@ -539,20 +541,20 @@ func (p *Poloniex) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, } orders = append(orders, order.Detail{ - ID: strconv.FormatInt(resp.Data[key][i].OrderNumber, 10), - OrderSide: orderSide, - Amount: resp.Data[key][i].Amount, - OrderDate: orderDate, - Price: resp.Data[key][i].Rate, - CurrencyPair: symbol, - Exchange: p.Name, + ID: strconv.FormatInt(resp.Data[key][i].OrderNumber, 10), + Side: orderSide, + Amount: resp.Data[key][i].Amount, + Date: orderDate, + Price: resp.Data[key][i].Rate, + Pair: symbol, + Exchange: p.Name, }) } } order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) - order.FilterOrdersByCurrencies(&orders, req.Currencies) - order.FilterOrdersBySide(&orders, req.OrderSide) + order.FilterOrdersByCurrencies(&orders, req.Pairs) + order.FilterOrdersBySide(&orders, req.Side) return orders, nil } @@ -574,7 +576,7 @@ func (p *Poloniex) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail, for i := range resp.Data[key] { orderSide := order.Side(strings.ToUpper(resp.Data[key][i].Type)) - orderDate, err := time.Parse(poloniexDateLayout, + orderDate, err := time.Parse(common.SimpleTimeFormat, resp.Data[key][i].Date) if err != nil { log.Errorf(log.ExchangeSys, @@ -586,19 +588,19 @@ func (p *Poloniex) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail, } orders = append(orders, order.Detail{ - ID: strconv.FormatInt(resp.Data[key][i].GlobalTradeID, 10), - OrderSide: orderSide, - Amount: resp.Data[key][i].Amount, - OrderDate: orderDate, - Price: resp.Data[key][i].Rate, - CurrencyPair: symbol, - Exchange: p.Name, + ID: strconv.FormatInt(resp.Data[key][i].GlobalTradeID, 10), + Side: orderSide, + Amount: resp.Data[key][i].Amount, + Date: orderDate, + Price: resp.Data[key][i].Rate, + Pair: symbol, + Exchange: p.Name, }) } } - order.FilterOrdersByCurrencies(&orders, req.Currencies) - order.FilterOrdersBySide(&orders, req.OrderSide) + order.FilterOrdersByCurrencies(&orders, req.Pairs) + order.FilterOrdersBySide(&orders, req.Side) return orders, nil } diff --git a/exchanges/request/request.go b/exchanges/request/request.go index c8106821..c3f40add 100644 --- a/exchanges/request/request.go +++ b/exchanges/request/request.go @@ -188,7 +188,10 @@ func (r *Requester) doRequest(req *http.Request, p *Item) error { string(contents)) } } - return json.Unmarshal(contents, p.Result) + if p.Result != nil { + return json.Unmarshal(contents, p.Result) + } + return nil } return fmt.Errorf("request.go error - failed to retry request %s", timeoutError) diff --git a/exchanges/sharedtestvalues/sharedtestvalues.go b/exchanges/sharedtestvalues/sharedtestvalues.go index 22732338..254e9156 100644 --- a/exchanges/sharedtestvalues/sharedtestvalues.go +++ b/exchanges/sharedtestvalues/sharedtestvalues.go @@ -12,7 +12,7 @@ const ( WebsocketResponseExtendedTimeout = (15 * time.Second) // WebsocketChannelOverrideCapacity used in websocket testing // Defines channel capacity as defaults size can block tests - WebsocketChannelOverrideCapacity = 20 + WebsocketChannelOverrideCapacity = 75 MockTesting = "Mock testing framework in use for %s exchange @ %s on REST endpoints only" LiveTesting = "Mock testing bypassed; live testing of REST endpoints in use for %s exchange @ %s" diff --git a/exchanges/websocket/wshandler/wshandler.go b/exchanges/websocket/wshandler/wshandler.go index 3631fbd7..39af1712 100644 --- a/exchanges/websocket/wshandler/wshandler.go +++ b/exchanges/websocket/wshandler/wshandler.go @@ -620,8 +620,8 @@ func (w *Websocket) CanUseAuthenticatedEndpoints() bool { return w.canUseAuthenticatedEndpoints } -// AddResponseWithID adds data to IDResponses with locks and a nil check -func (w *WebsocketConnection) AddResponseWithID(id int64, data []byte) { +// SetResponseIDAndData adds data to IDResponses with locks and a nil check +func (w *WebsocketConnection) SetResponseIDAndData(id int64, data []byte) { w.Lock() defer w.Unlock() if w.IDResponses == nil { @@ -734,6 +734,7 @@ func (w *WebsocketConnection) SendMessageReturnResponse(id int64, request interf if err != nil { return nil, err } + w.SetResponseIDAndData(id, nil) var wg sync.WaitGroup wg.Add(1) go w.WaitForResult(id, &wg) @@ -748,6 +749,20 @@ func (w *WebsocketConnection) SendMessageReturnResponse(id int64, request interf return w.IDResponses[id], nil } +// IsIDWaitingForResponse will verify whether the websocket is awaiting +// a response with a correlating ID. If true, the datahandler won't process +// the data, and instead will be processed by the wrapper function +func (w *WebsocketConnection) IsIDWaitingForResponse(id int64) bool { + w.Lock() + defer w.Unlock() + for k := range w.IDResponses { + if k == id && w.IDResponses[k] == nil { + return true + } + } + return false +} + // WaitForResult will keep checking w.IDResponses for a response ID // If the timer expires, it will return without func (w *WebsocketConnection) WaitForResult(id int64, wg *sync.WaitGroup) { @@ -760,7 +775,7 @@ func (w *WebsocketConnection) WaitForResult(id int64, wg *sync.WaitGroup) { default: w.Lock() for k := range w.IDResponses { - if k == id { + if k == id && w.IDResponses[k] != nil { w.Unlock() if !timer.Stop() { select { diff --git a/exchanges/websocket/wshandler/wshandler_test.go b/exchanges/websocket/wshandler/wshandler_test.go index 3365ebd6..d59c55e8 100644 --- a/exchanges/websocket/wshandler/wshandler_test.go +++ b/exchanges/websocket/wshandler/wshandler_test.go @@ -695,11 +695,33 @@ func TestParseBinaryResponse(t *testing.T) { } } -// TestAddResponseWithID logic test -func TestAddResponseWithID(t *testing.T) { +// TestSetResponseIDAndData logic test +func TestSetResponseIDAndData(t *testing.T) { wc.IDResponses = nil - wc.AddResponseWithID(0, []byte("hi")) - wc.AddResponseWithID(1, []byte("hi")) + wc.SetResponseIDAndData(0, nil) + wc.SetResponseIDAndData(1, []byte("hi")) + if len(wc.IDResponses) != 2 { + t.Error("Expected 2 entries") + } +} + +// TestIsIDWaitingForResponse logic test +func TestIsIDWaitingForResponse(t *testing.T) { + wc.IDResponses = nil + wc.SetResponseIDAndData(0, nil) + wc.SetResponseIDAndData(1, []byte("hi")) + if len(wc.IDResponses) != 2 { + t.Error("Expected 2 entries") + } + if !wc.IsIDWaitingForResponse(0) { + t.Error("Expected true") + } + if wc.IsIDWaitingForResponse(2) { + t.Error("Expected false") + } + if wc.IsIDWaitingForResponse(1337) { + t.Error("Expected false") + } } // readMessages helper func @@ -722,7 +744,7 @@ func readMessages(wc *WebsocketConnection, t *testing.T) { return } if incoming.RequestID > 0 { - wc.AddResponseWithID(incoming.RequestID, resp.Raw) + wc.SetResponseIDAndData(incoming.RequestID, resp.Raw) return } } diff --git a/exchanges/websocket/wshandler/wshandler_types.go b/exchanges/websocket/wshandler/wshandler_types.go index 31e8b4fe..6652d6d4 100644 --- a/exchanges/websocket/wshandler/wshandler_types.go +++ b/exchanges/websocket/wshandler/wshandler_types.go @@ -7,6 +7,7 @@ import ( "github.com/gorilla/websocket" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/protocol" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wsorderbook" ) @@ -21,6 +22,7 @@ const ( WebsocketNotAuthenticatedUsingRest = "%v - Websocket not authenticated, using REST" Ping = "ping" Pong = "pong" + UnhandledMessage = " - Unhandled websocket message: " ) // Websocket defines a return type for websocket connections via the interface @@ -105,10 +107,10 @@ type TradeData struct { CurrencyPair currency.Pair AssetType asset.Item Exchange string - EventType string + EventType order.Type Price float64 Amount float64 - Side string + Side order.Side } // FundingData defines funding data @@ -120,7 +122,7 @@ type FundingData struct { Amount float64 Rate float64 Period int64 - Side string + Side order.Side } // KlineData defines kline feed @@ -174,3 +176,7 @@ type WebsocketPingHandler struct { Message []byte Delay time.Duration } + +type UnhandledMessageWarning struct { + Message string +} diff --git a/exchanges/yobit/yobit_test.go b/exchanges/yobit/yobit_test.go index 738a667b..c9ec0ca2 100644 --- a/exchanges/yobit/yobit_test.go +++ b/exchanges/yobit/yobit_test.go @@ -326,8 +326,8 @@ func TestFormatWithdrawPermissions(t *testing.T) { func TestGetActiveOrders(t *testing.T) { var getOrdersRequest = order.GetOrdersRequest{ - OrderType: order.AnyType, - Currencies: []currency.Pair{currency.NewPair(currency.LTC, + Type: order.AnyType, + Pairs: []currency.Pair{currency.NewPair(currency.LTC, currency.BTC)}, } @@ -341,8 +341,8 @@ func TestGetActiveOrders(t *testing.T) { func TestGetOrderHistory(t *testing.T) { var getOrdersRequest = order.GetOrdersRequest{ - OrderType: order.AnyType, - Currencies: []currency.Pair{currency.NewPair(currency.LTC, + Type: order.AnyType, + Pairs: []currency.Pair{currency.NewPair(currency.LTC, currency.BTC)}, StartTicks: time.Unix(0, 0), EndTicks: time.Unix(math.MaxInt64, 0), @@ -373,11 +373,11 @@ func TestSubmitOrder(t *testing.T) { Base: currency.BTC, Quote: currency.USD, }, - OrderSide: order.Buy, - OrderType: order.Limit, - Price: 1, - Amount: 1, - ClientID: "meowOrder", + Side: order.Buy, + Type: order.Limit, + Price: 1, + Amount: 1, + ClientID: "meowOrder", } response, err := y.SubmitOrder(orderSubmission) if areTestAPIKeysSet() && (err != nil || !response.IsOrderPlaced) { @@ -394,10 +394,10 @@ func TestCancelExchangeOrder(t *testing.T) { currencyPair := currency.NewPair(currency.LTC, currency.BTC) var orderCancellation = &order.Cancel{ - OrderID: "1", + ID: "1", WalletAddress: core.BitcoinDonationAddress, AccountID: "1", - CurrencyPair: currencyPair, + Pair: currencyPair, } err := y.CancelOrder(orderCancellation) @@ -416,10 +416,10 @@ func TestCancelAllExchangeOrders(t *testing.T) { currencyPair := currency.NewPair(currency.LTC, currency.BTC) var orderCancellation = &order.Cancel{ - OrderID: "1", + ID: "1", WalletAddress: core.BitcoinDonationAddress, AccountID: "1", - CurrencyPair: currencyPair, + Pair: currencyPair, } resp, err := y.CancelAllOrders(orderCancellation) diff --git a/exchanges/yobit/yobit_wrapper.go b/exchanges/yobit/yobit_wrapper.go index a99e8ab7..2aa4dc8d 100644 --- a/exchanges/yobit/yobit_wrapper.go +++ b/exchanges/yobit/yobit_wrapper.go @@ -334,12 +334,12 @@ func (y *Yobit) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) { return submitOrderResponse, err } - if s.OrderType != order.Limit { + if s.Type != order.Limit { return submitOrderResponse, errors.New("only limit orders are allowed") } response, err := y.Trade(s.Pair.String(), - s.OrderSide.String(), + s.Side.String(), s.Amount, s.Price) if err != nil { @@ -361,7 +361,7 @@ func (y *Yobit) ModifyOrder(action *order.Modify) (string, error) { // CancelOrder cancels an order by its corresponding ID number func (y *Yobit) CancelOrder(order *order.Cancel) error { - orderIDInt, err := strconv.ParseInt(order.OrderID, 10, 64) + orderIDInt, err := strconv.ParseInt(order.ID, 10, 64) if err != nil { return err } @@ -463,8 +463,8 @@ func (y *Yobit) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) { // GetActiveOrders retrieves any orders that are active/open func (y *Yobit) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, error) { var orders []order.Detail - for x := range req.Currencies { - fCurr := y.FormatExchangeCurrency(req.Currencies[x], asset.Spot).String() + for x := range req.Pairs { + fCurr := y.FormatExchangeCurrency(req.Pairs[x], asset.Spot).String() resp, err := y.GetOpenOrders(fCurr) if err != nil { return nil, err @@ -476,19 +476,19 @@ func (y *Yobit) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, er orderDate := time.Unix(int64(resp[id].TimestampCreated), 0) side := order.Side(strings.ToUpper(resp[id].Type)) orders = append(orders, order.Detail{ - ID: id, - Amount: resp[id].Amount, - Price: resp[id].Rate, - OrderSide: side, - OrderDate: orderDate, - CurrencyPair: symbol, - Exchange: y.Name, + ID: id, + Amount: resp[id].Amount, + Price: resp[id].Rate, + Side: side, + Date: orderDate, + Pair: symbol, + Exchange: y.Name, }) } } order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) - order.FilterOrdersBySide(&orders, req.OrderSide) + order.FilterOrdersBySide(&orders, req.Side) return orders, nil } @@ -496,14 +496,14 @@ func (y *Yobit) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, er // Can Limit response to specific order status func (y *Yobit) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail, error) { var allOrders []TradeHistory - for x := range req.Currencies { + for x := range req.Pairs { resp, err := y.GetTradeHistory(0, 10000, math.MaxInt64, req.StartTicks.Unix(), req.EndTicks.Unix(), "DESC", - y.FormatExchangeCurrency(req.Currencies[x], asset.Spot).String()) + y.FormatExchangeCurrency(req.Pairs[x], asset.Spot).String()) if err != nil { return nil, err } @@ -520,17 +520,17 @@ func (y *Yobit) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail, er orderDate := time.Unix(int64(allOrders[i].Timestamp), 0) side := order.Side(strings.ToUpper(allOrders[i].Type)) orders = append(orders, order.Detail{ - ID: strconv.FormatFloat(allOrders[i].OrderID, 'f', -1, 64), - Amount: allOrders[i].Amount, - Price: allOrders[i].Rate, - OrderSide: side, - OrderDate: orderDate, - CurrencyPair: symbol, - Exchange: y.Name, + ID: strconv.FormatFloat(allOrders[i].OrderID, 'f', -1, 64), + Amount: allOrders[i].Amount, + Price: allOrders[i].Rate, + Side: side, + Date: orderDate, + Pair: symbol, + Exchange: y.Name, }) } - order.FilterOrdersBySide(&orders, req.OrderSide) + order.FilterOrdersBySide(&orders, req.Side) return orders, nil } diff --git a/exchanges/zb/zb_test.go b/exchanges/zb/zb_test.go index 41bc8976..76b09996 100644 --- a/exchanges/zb/zb_test.go +++ b/exchanges/zb/zb_test.go @@ -16,6 +16,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" "github.com/thrasher-corp/gocryptotrader/exchanges/order" + "github.com/thrasher-corp/gocryptotrader/exchanges/sharedtestvalues" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" "github.com/thrasher-corp/gocryptotrader/portfolio/withdraw" ) @@ -45,12 +46,12 @@ func TestMain(m *testing.M) { zbConfig.API.AuthenticatedWebsocketSupport = true zbConfig.API.Credentials.Key = apiKey zbConfig.API.Credentials.Secret = apiSecret - err = z.Setup(zbConfig) if err != nil { log.Fatal("ZB setup error", err) } - + z.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride() + z.Websocket.TrafficAlert = sharedtestvalues.GetWebsocketStructChannelOverride() os.Exit(m.Run()) } @@ -75,7 +76,7 @@ func setupWsAuth(t *testing.T) { } z.Websocket.DataHandler = make(chan interface{}, 11) z.Websocket.TrafficAlert = make(chan struct{}, 11) - go z.WsHandleData() + go z.wsReadData() wsSetupRan = true } @@ -279,8 +280,8 @@ func TestFormatWithdrawPermissions(t *testing.T) { func TestGetActiveOrders(t *testing.T) { var getOrdersRequest = order.GetOrdersRequest{ - OrderType: order.AnyType, - Currencies: []currency.Pair{currency.NewPair(currency.XRP, + Type: order.AnyType, + Pairs: []currency.Pair{currency.NewPair(currency.XRP, currency.USDT)}, } @@ -294,9 +295,9 @@ func TestGetActiveOrders(t *testing.T) { func TestGetOrderHistory(t *testing.T) { var getOrdersRequest = order.GetOrdersRequest{ - OrderType: order.AnyType, - OrderSide: order.Buy, - Currencies: []currency.Pair{currency.NewPair(currency.LTC, + Type: order.AnyType, + Side: order.Buy, + Pairs: []currency.Pair{currency.NewPair(currency.LTC, currency.BTC)}, } @@ -327,11 +328,11 @@ func TestSubmitOrder(t *testing.T) { Base: currency.XRP, Quote: currency.USDT, }, - OrderSide: order.Buy, - OrderType: order.Limit, - Price: 1, - Amount: 1, - ClientID: "meowOrder", + Side: order.Buy, + Type: order.Limit, + Price: 1, + Amount: 1, + ClientID: "meowOrder", } response, err := z.SubmitOrder(orderSubmission) if areTestAPIKeysSet() && (err != nil || !response.IsOrderPlaced) { @@ -348,10 +349,10 @@ func TestCancelExchangeOrder(t *testing.T) { currencyPair := currency.NewPair(currency.XRP, currency.USDT) var orderCancellation = &order.Cancel{ - OrderID: "1", + ID: "1", WalletAddress: core.BitcoinDonationAddress, AccountID: "1", - CurrencyPair: currencyPair, + Pair: currencyPair, } err := z.CancelOrder(orderCancellation) @@ -370,10 +371,10 @@ func TestCancelAllExchangeOrders(t *testing.T) { currencyPair := currency.NewPair(currency.XRP, currency.USDT) var orderCancellation = &order.Cancel{ - OrderID: "1", + ID: "1", WalletAddress: core.BitcoinDonationAddress, AccountID: "1", - CurrencyPair: currencyPair, + Pair: currencyPair, } resp, err := z.CancelAllOrders(orderCancellation) @@ -598,3 +599,242 @@ func TestWsGetOrdersIgnoreTradeType(t *testing.T) { t.Fatal(err) } } + +func TestWsMarketConfig(t *testing.T) { + pressXToJSON := []byte(`{ + "code":1000, + "data":{ + "btc_usdt":{ + "amountScale":4, + "priceScale":2 + }, + "bcc_usdt":{ + "amountScale":3, + "priceScale":2 + } + }, + "success":true, + "channel":"markets", + "message":"操作成功。" +}`) + err := z.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsTicker(t *testing.T) { + pressXToJSON := []byte(`{ + "channel": "ltcbtc_ticker", + "date": "1472800466093", + "no": "1337", + "ticker": { + "buy": "3826.94", + "high": "3838.22", + "last": "3826.94", + "low": "3802.0", + "sell": "3828.25", + "vol": "90151.83" + } +}`) + err := z.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsOrderbook(t *testing.T) { + pressXToJSON := []byte(`{ + "asks": [ + [ + 3846.94, + 0.659 + ] + ], + "bids": [ + [ + 3826.94, + 4.843 + ] + ], + "channel": "ltcbtc_depth", + "no": "1337" +}`) + err := z.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsTrades(t *testing.T) { + pressXToJSON := []byte(`{"data":[{"date":1581473835,"amount":"13.620","price":"242.89","trade_type":"bid","type":"buy","tid":703896035},{"date":1581473835,"amount":"0.156","price":"242.89","trade_type":"bid","type":"buy","tid":703896036}],"dataType":"trades","channel":"ethusdt_trades"}`) + err := z.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsPlaceOrderJSON(t *testing.T) { + pressXToJSON := []byte(`{"message":"操作成功。","no":"1337","data":"{"entrustId":201711133673}","code":1000,"channel":"btcusdt_order","success":true}`) + err := z.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsCancelOrderJSON(t *testing.T) { + pressXToJSON := []byte(`{ + "success": true, + "code": 1000, + "channel": "ltcbtc_cancelorder", + "message": "操作成功。", + "no": "1337" +}`) + err := z.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsGetOrderJSON(t *testing.T) { + pressXToJSON := []byte(`{ + "success": true, + "code": 1000, + "data": { + "currency": "ltc_btc", + "id": "20160902387645980", + "price": 100, + "status": 0, + "total_amount": 0.01, + "trade_amount": 0, + "trade_date": 1472814905567, + "trade_money": 0, + "type": 1 + }, + "channel": "ltcbtc_getorder", + "message": "操作成功。", + "no": "1337" +}`) + err := z.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsGetOrdersJSON(t *testing.T) { + pressXToJSON := []byte(`{ + "success": true, + "code": 1000, + "data": [ + { + "currency": "ltc_btc", + "id": "20160901385862136", + "price": 3700, + "status": 0, + "total_amount": 1.845, + "trade_amount": 0, + "trade_date": 1472706387742, + "trade_money": 0, + "type": 1 + } + ], + "channel": "ltcbtc_getorders", + "message": "操作成功。", + "no": "1337" +}`) + err := z.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsGetOrderIgnoreTypeJSON(t *testing.T) { + pressXToJSON := []byte(`{ + "success": true, + "code": 1000, + "data": [ + { + "currency": "ltc_btc", + "id": "20160901385862136", + "price": 3700, + "status": 0, + "total_amount": 1.845, + "trade_amount": 0, + "trade_date": 1472706387742, + "trade_money": 0, + "type": 1 + } + ], + "channel": "ltcbtc_getordersignoretradetype", + "message": "操作成功。", + "no": "1337" +}`) + err := z.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsGetUserInfo(t *testing.T) { + pressXToJSON := []byte(`{ + "message": "操作成功", + "no": "15207605119", + "data": { + "coins": [ + { + "freez": "1.35828369", + "enName": "BTC", + "unitDecimal": 8, + "cnName": "BTC", + "unitTag": "฿", + "available": "0.72771906", + "key": "btc" + }, + { + "freez": "0.011", + "enName": "LTC", + "unitDecimal": 8, + "cnName": "LTC", + "unitTag": "Ł", + "available": "3.51859814", + "key": "ltc" + } + ], + "base": { + "username": "15207605119", + "trade_password_enabled": true, + "auth_google_enabled": true, + "auth_mobile_enabled": true + } + }, + "code": 1000, + "channel": "getaccountinfo", + "success": true +}`) + err := z.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsGetSubUsersResponse(t *testing.T) { + pressXToJSON := []byte(`{"success": true,"code": 1000,"channel": "getSubUserList","message": "[{"isOpenApi": false,"memo": "1","userName": "15914665280@1","userId": 110980,"isFreez": false}, {"isOpenApi": false,"memo": "2","userName": "15914665280@2","userId": 110984,"isFreez": false}, {"isOpenApi": false,"memo": "test3","userName": "15914665280@3","userId": 111014,"isFreez": false}]","no": "0"}`) + err := z.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestWsCreateSubUserResponse(t *testing.T) { + pressXToJSON := []byte(`{ + "success": true, + "code": 1000, + "channel": "createSubUserKey", + "message": "{"apiKey ":"41 bf75f9 - 525e-4876 - 8257 - b880a938d4d2 ","apiSecret ":"046 b4706fe88b5728991274962d7fc46b4779c0c"}", + "no": "1337" +}`) + err := z.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} diff --git a/exchanges/zb/zb_websocket.go b/exchanges/zb/zb_websocket.go index 048cd4eb..e9db6453 100644 --- a/exchanges/zb/zb_websocket.go +++ b/exchanges/zb/zb_websocket.go @@ -6,6 +6,7 @@ import ( "fmt" "net/http" "regexp" + "strconv" "strings" "time" @@ -14,6 +15,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" @@ -37,17 +39,16 @@ func (z *ZB) WsConnect() error { return err } - go z.WsHandleData() + go z.wsReadData() z.GenerateDefaultSubscriptions() return nil } -// WsHandleData handles all the websocket data coming from the websocket +// wsReadData handles all the websocket data coming from the websocket // connection -func (z *ZB) WsHandleData() { +func (z *ZB) wsReadData() { z.Websocket.Wg.Add(1) - defer func() { z.Websocket.Wg.Done() }() @@ -63,130 +64,177 @@ func (z *ZB) WsHandleData() { return } z.Websocket.TrafficAlert <- struct{}{} - fixedJSON := z.wsFixInvalidJSON(resp.Raw) - var result Generic - err = json.Unmarshal(fixedJSON, &result) + err = z.wsHandleData(resp.Raw) if err != nil { z.Websocket.DataHandler <- err - continue - } - if result.No > 0 { - z.WebsocketConn.AddResponseWithID(result.No, fixedJSON) - continue - } - if result.Code > 0 && result.Code != 1000 { - z.Websocket.DataHandler <- fmt.Errorf("%v request failed, message: %v, error code: %v", z.Name, result.Message, wsErrCodes[result.Code]) - continue - } - switch { - case strings.Contains(result.Channel, "markets"): - var markets Markets - err := json.Unmarshal(result.Data, &markets) - if err != nil { - z.Websocket.DataHandler <- err - continue - } - - case strings.Contains(result.Channel, "ticker"): - cPair := strings.Split(result.Channel, "_") - var wsTicker WsTicker - err := json.Unmarshal(fixedJSON, &wsTicker) - if err != nil { - z.Websocket.DataHandler <- err - continue - } - - z.Websocket.DataHandler <- &ticker.Price{ - ExchangeName: z.Name, - Close: wsTicker.Data.Last, - Volume: wsTicker.Data.Volume24Hr, - High: wsTicker.Data.High, - Low: wsTicker.Data.Low, - Last: wsTicker.Data.Last, - Bid: wsTicker.Data.Buy, - Ask: wsTicker.Data.Sell, - LastUpdated: time.Unix(0, wsTicker.Date*int64(time.Millisecond)), - AssetType: asset.Spot, - Pair: currency.NewPairFromString(cPair[0]), - } - - case strings.Contains(result.Channel, "depth"): - var depth WsDepth - err := json.Unmarshal(fixedJSON, &depth) - if err != nil { - z.Websocket.DataHandler <- err - continue - } - - var asks []orderbook.Item - for i := range depth.Asks { - asks = append(asks, orderbook.Item{ - Amount: depth.Asks[i][1].(float64), - Price: depth.Asks[i][0].(float64), - }) - } - - var bids []orderbook.Item - for i := range depth.Bids { - bids = append(bids, orderbook.Item{ - Amount: depth.Bids[i][1].(float64), - Price: depth.Bids[i][0].(float64), - }) - } - - channelInfo := strings.Split(result.Channel, "_") - cPair := currency.NewPairFromString(channelInfo[0]) - var newOrderBook orderbook.Base - newOrderBook.Asks = asks - newOrderBook.Bids = bids - newOrderBook.AssetType = asset.Spot - newOrderBook.Pair = cPair - newOrderBook.ExchangeName = z.Name - - err = z.Websocket.Orderbook.LoadSnapshot(&newOrderBook) - if err != nil { - z.Websocket.DataHandler <- err - continue - } - - z.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{ - Pair: cPair, - Asset: asset.Spot, - Exchange: z.Name, - } - - case strings.Contains(result.Channel, "trades"): - var trades WsTrades - err := json.Unmarshal(fixedJSON, &trades) - if err != nil { - z.Websocket.DataHandler <- err - continue - } - // Most up to date trade - if len(trades.Data) == 0 { - continue - } - t := trades.Data[len(trades.Data)-1] - - channelInfo := strings.Split(result.Channel, "_") - cPair := currency.NewPairFromString(channelInfo[0]) - z.Websocket.DataHandler <- wshandler.TradeData{ - Timestamp: time.Unix(t.Date, 0), - CurrencyPair: cPair, - AssetType: asset.Spot, - Exchange: z.Name, - Price: t.Price, - Amount: t.Amount, - Side: t.TradeType, - } - default: - z.Websocket.DataHandler <- errors.New("zb_websocket.go error - unhandled websocket response") - continue } } } } +func (z *ZB) wsHandleData(respRaw []byte) error { + fixedJSON := z.wsFixInvalidJSON(respRaw) + var result Generic + err := json.Unmarshal(fixedJSON, &result) + if err != nil { + return err + } + if result.No > 0 { + if z.WebsocketConn.IsIDWaitingForResponse(result.No) { + z.WebsocketConn.SetResponseIDAndData(result.No, respRaw) + return nil + } + } + if result.Code > 0 && result.Code != 1000 { + return fmt.Errorf("%v request failed, message: %v, error code: %v", z.Name, result.Message, wsErrCodes[result.Code]) + } + switch { + case strings.Contains(result.Channel, "markets"): + var markets Markets + err := json.Unmarshal(result.Data, &markets) + if err != nil { + return err + } + + case strings.Contains(result.Channel, "ticker"): + cPair := strings.Split(result.Channel, "_") + var wsTicker WsTicker + err := json.Unmarshal(fixedJSON, &wsTicker) + if err != nil { + return err + } + + z.Websocket.DataHandler <- &ticker.Price{ + ExchangeName: z.Name, + Close: wsTicker.Data.Last, + Volume: wsTicker.Data.Volume24Hr, + High: wsTicker.Data.High, + Low: wsTicker.Data.Low, + Last: wsTicker.Data.Last, + Bid: wsTicker.Data.Buy, + Ask: wsTicker.Data.Sell, + LastUpdated: time.Unix(0, wsTicker.Date*int64(time.Millisecond)), + AssetType: asset.Spot, + Pair: currency.NewPairFromString(cPair[0]), + } + + case strings.Contains(result.Channel, "depth"): + var depth WsDepth + err := json.Unmarshal(fixedJSON, &depth) + if err != nil { + return err + } + + var asks []orderbook.Item + for i := range depth.Asks { + asks = append(asks, orderbook.Item{ + Amount: depth.Asks[i][1].(float64), + Price: depth.Asks[i][0].(float64), + }) + } + + var bids []orderbook.Item + for i := range depth.Bids { + bids = append(bids, orderbook.Item{ + Amount: depth.Bids[i][1].(float64), + Price: depth.Bids[i][0].(float64), + }) + } + + channelInfo := strings.Split(result.Channel, "_") + cPair := currency.NewPairFromString(channelInfo[0]) + var newOrderBook orderbook.Base + newOrderBook.Asks = asks + newOrderBook.Bids = bids + newOrderBook.AssetType = asset.Spot + newOrderBook.Pair = cPair + newOrderBook.ExchangeName = z.Name + + err = z.Websocket.Orderbook.LoadSnapshot(&newOrderBook) + if err != nil { + return err + } + + z.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{ + Pair: cPair, + Asset: asset.Spot, + Exchange: z.Name, + } + case strings.Contains(result.Channel, "_order"): + cPair := strings.Split(result.Channel, "_") + var o WsSubmitOrderResponse + err := json.Unmarshal(fixedJSON, &o) + if err != nil { + return err + } + if !o.Success { + return fmt.Errorf("%s - Order %v failed to be placed. %s", z.Name, o.Data.EntrustID, respRaw) + } + p := currency.NewPairFromString(cPair[0]) + var a asset.Item + a, err = z.GetPairAssetType(p) + if err != nil { + return err + } + z.Websocket.DataHandler <- &order.Detail{ + Exchange: z.Name, + ID: strconv.FormatInt(o.Data.EntrustID, 10), + Pair: p, + AssetType: a, + } + case strings.Contains(result.Channel, "_cancelorder"): + cPair := strings.Split(result.Channel, "_") + var o WsSubmitOrderResponse + err := json.Unmarshal(fixedJSON, &o) + if err != nil { + return err + } + if !o.Success { + return fmt.Errorf("%s - Order %v failed to be cancelled. %s", z.Name, o.Data.EntrustID, respRaw) + } + z.Websocket.DataHandler <- &order.Modify{ + Exchange: z.Name, + ID: strconv.FormatInt(o.Data.EntrustID, 10), + Pair: currency.NewPairFromString(cPair[0]), + Status: order.Cancelled, + } + case strings.Contains(result.Channel, "trades"): + var trades WsTrades + err := json.Unmarshal(fixedJSON, &trades) + if err != nil { + return err + } + // Most up to date trade + if len(trades.Data) == 0 { + return errors.New(z.Name + " - Empty websocket trade data received: " + string(fixedJSON)) + } + t := trades.Data[len(trades.Data)-1] + + channelInfo := strings.Split(result.Channel, "_") + cPair := currency.NewPairFromString(channelInfo[0]) + tSide, err := order.StringToOrderSide(t.TradeType) + if err != nil { + z.Websocket.DataHandler <- order.ClassificationError{ + Exchange: z.Name, + Err: err, + } + } + z.Websocket.DataHandler <- wshandler.TradeData{ + Timestamp: time.Unix(t.Date, 0), + CurrencyPair: cPair, + AssetType: asset.Spot, + Exchange: z.Name, + Price: t.Price, + Amount: t.Amount, + Side: tSide, + } + default: + z.Websocket.DataHandler <- wshandler.UnhandledMessageWarning{Message: z.Name + wshandler.UnhandledMessage + string(respRaw)} + return nil + } + return nil +} + // GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions() func (z *ZB) GenerateDefaultSubscriptions() { var subscriptions []wshandler.WebsocketChannelSubscription diff --git a/exchanges/zb/zb_wrapper.go b/exchanges/zb/zb_wrapper.go index 74f60e1a..80503f6f 100644 --- a/exchanges/zb/zb_wrapper.go +++ b/exchanges/zb/zb_wrapper.go @@ -100,6 +100,8 @@ func (z *ZB) SetDefaults() { CancelOrder: true, SubmitOrder: true, MessageCorrelation: true, + GetOrders: true, + GetOrder: true, }, WithdrawPermissions: exchange.AutoWithdrawCrypto | exchange.NoFiatWithdrawals, @@ -385,7 +387,7 @@ func (z *ZB) SubmitOrder(o *order.Submit) (order.SubmitResponse, error) { } if z.Websocket.CanUseAuthenticatedWebsocketForWrapper() { var isBuyOrder int64 - if o.OrderSide == order.Buy { + if o.Side == order.Buy { isBuyOrder = 1 } else { isBuyOrder = 0 @@ -398,7 +400,7 @@ func (z *ZB) SubmitOrder(o *order.Submit) (order.SubmitResponse, error) { submitOrderResponse.OrderID = strconv.FormatInt(response.Data.EntrustID, 10) } else { var oT SpotNewOrderRequestParamsType - if o.OrderSide == order.Buy { + if o.Side == order.Buy { oT = SpotNewOrderRequestParamsTypeBuy } else { oT = SpotNewOrderRequestParamsTypeSell @@ -420,7 +422,7 @@ func (z *ZB) SubmitOrder(o *order.Submit) (order.SubmitResponse, error) { } } submitOrderResponse.IsOrderPlaced = true - if o.OrderType == order.Market { + if o.Type == order.Market { submitOrderResponse.FullyMatched = true } return submitOrderResponse, nil @@ -434,23 +436,23 @@ func (z *ZB) ModifyOrder(action *order.Modify) (string, error) { // CancelOrder cancels an order by its corresponding ID number func (z *ZB) CancelOrder(o *order.Cancel) error { - orderIDInt, err := strconv.ParseInt(o.OrderID, 10, 64) + orderIDInt, err := strconv.ParseInt(o.ID, 10, 64) if err != nil { return err } if z.Websocket.CanUseAuthenticatedWebsocketForWrapper() { var response *WsCancelOrderResponse - response, err = z.wsCancelOrder(o.CurrencyPair, orderIDInt) + response, err = z.wsCancelOrder(o.Pair, orderIDInt) if err != nil { return err } if !response.Success { - return fmt.Errorf("%v - Could not cancel order %v", z.Name, o.OrderID) + return fmt.Errorf("%v - Could not cancel order %v", z.Name, o.ID) } return nil } - return z.CancelExistingOrder(orderIDInt, z.FormatExchangeCurrency(o.CurrencyPair, + return z.CancelExistingOrder(orderIDInt, z.FormatExchangeCurrency(o.Pair, o.AssetType).String()) } @@ -486,8 +488,8 @@ func (z *ZB) CancelAllOrders(_ *order.Cancel) (order.CancelAllResponse, error) { for i := range allOpenOrders { err := z.CancelOrder(&order.Cancel{ - OrderID: strconv.FormatInt(allOpenOrders[i].ID, 10), - CurrencyPair: currency.NewPairFromString(allOpenOrders[i].Currency), + ID: strconv.FormatInt(allOpenOrders[i].ID, 10), + Pair: currency.NewPairFromString(allOpenOrders[i].Currency), }) if err != nil { cancelAllOrdersResponse.Status[strconv.FormatInt(allOpenOrders[i].ID, 10)] = err.Error() @@ -555,9 +557,9 @@ func (z *ZB) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) { // This function is not concurrency safe due to orderSide/orderType maps func (z *ZB) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, error) { var allOrders []Order - for x := range req.Currencies { + for x := range req.Pairs { for i := int64(1); ; i++ { - fPair := z.FormatExchangeCurrency(req.Currencies[x], asset.Spot).String() + fPair := z.FormatExchangeCurrency(req.Pairs[x], asset.Spot).String() resp, err := z.GetUnfinishedOrdersIgnoreTradeType(fPair, i, 10) if err != nil { if strings.Contains(err.Error(), "3001") { @@ -585,18 +587,18 @@ 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: strconv.FormatInt(allOrders[i].ID, 10), - Amount: allOrders[i].TotalAmount, - Exchange: z.Name, - OrderDate: orderDate, - Price: allOrders[i].Price, - OrderSide: orderSide, - CurrencyPair: symbol, + ID: strconv.FormatInt(allOrders[i].ID, 10), + Amount: allOrders[i].TotalAmount, + Exchange: z.Name, + Date: orderDate, + Price: allOrders[i].Price, + Side: orderSide, + Pair: symbol, }) } order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) - order.FilterOrdersBySide(&orders, req.OrderSide) + order.FilterOrdersBySide(&orders, req.Side) return orders, nil } @@ -604,7 +606,7 @@ func (z *ZB) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, error // Can Limit response to specific order status // This function is not concurrency safe due to orderSide/orderType maps func (z *ZB) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail, error) { - if req.OrderSide == order.AnySide || req.OrderSide == "" { + if req.Side == order.AnySide || req.Side == "" { return nil, errors.New("specific order side is required") } var allOrders []Order @@ -612,9 +614,9 @@ func (z *ZB) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail, error var side int64 if z.Websocket.CanUseAuthenticatedWebsocketForWrapper() { - for x := range req.Currencies { + for x := range req.Pairs { for y := int64(1); ; y++ { - resp, err := z.wsGetOrdersIgnoreTradeType(req.Currencies[x], y, 10) + resp, err := z.wsGetOrdersIgnoreTradeType(req.Pairs[x], y, 10) if err != nil { return nil, err } @@ -625,12 +627,12 @@ func (z *ZB) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail, error } } } else { - if req.OrderSide == order.Buy { + if req.Side == order.Buy { side = 1 } - for x := range req.Currencies { + for x := range req.Pairs { for y := int64(1); ; y++ { - fPair := z.FormatExchangeCurrency(req.Currencies[x], asset.Spot).String() + fPair := z.FormatExchangeCurrency(req.Pairs[x], asset.Spot).String() resp, err := z.GetOrders(fPair, y, side) if err != nil { return nil, err @@ -652,13 +654,13 @@ 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: strconv.FormatInt(allOrders[i].ID, 10), - Amount: allOrders[i].TotalAmount, - Exchange: z.Name, - OrderDate: orderDate, - Price: allOrders[i].Price, - OrderSide: orderSide, - CurrencyPair: symbol, + ID: strconv.FormatInt(allOrders[i].ID, 10), + Amount: allOrders[i].TotalAmount, + Exchange: z.Name, + Date: orderDate, + Price: allOrders[i].Price, + Side: orderSide, + Pair: symbol, }) } diff --git a/gctscript/modules/gct/exchange.go b/gctscript/modules/gct/exchange.go index 779f98a4..80f96d4a 100644 --- a/gctscript/modules/gct/exchange.go +++ b/gctscript/modules/gct/exchange.go @@ -260,15 +260,15 @@ func ExchangeOrderQuery(args ...objects.Object) (objects.Object, error) { data["exchange"] = &objects.String{Value: orderDetails.Exchange} data["id"] = &objects.String{Value: orderDetails.ID} data["accountid"] = &objects.String{Value: orderDetails.AccountID} - data["currencypair"] = &objects.String{Value: orderDetails.CurrencyPair.String()} + data["currencypair"] = &objects.String{Value: orderDetails.Pair.String()} data["price"] = &objects.Float{Value: orderDetails.Price} data["amount"] = &objects.Float{Value: orderDetails.Amount} data["amountexecuted"] = &objects.Float{Value: orderDetails.ExecutedAmount} data["amountremaining"] = &objects.Float{Value: orderDetails.RemainingAmount} data["fee"] = &objects.Float{Value: orderDetails.Fee} - data["side"] = &objects.String{Value: orderDetails.OrderSide.String()} - data["type"] = &objects.String{Value: orderDetails.OrderType.String()} - data["date"] = &objects.String{Value: orderDetails.OrderDate.String()} + data["side"] = &objects.String{Value: orderDetails.Side.String()} + data["type"] = &objects.String{Value: orderDetails.Type.String()} + data["date"] = &objects.String{Value: orderDetails.Date.String()} data["status"] = &objects.String{Value: orderDetails.Status.String()} data["trades"] = &tradeHistory @@ -344,12 +344,12 @@ func ExchangeOrderSubmit(args ...objects.Object) (objects.Object, error) { pair := currency.NewPairDelimiter(currencyPair, delimiter) tempSubmit := &order.Submit{ - Pair: pair, - OrderType: order.Type(orderType), - OrderSide: order.Side(orderSide), - Price: orderPrice, - Amount: orderAmount, - ClientID: orderClientID, + Pair: pair, + Type: order.Type(orderType), + Side: order.Side(orderSide), + Price: orderPrice, + Amount: orderAmount, + ClientID: orderClientID, } err := tempSubmit.Validate() @@ -357,7 +357,7 @@ func ExchangeOrderSubmit(args ...objects.Object) (objects.Object, error) { return nil, err } - rtn, err := wrappers.GetWrapper().SubmitOrder(exchangeName, tempSubmit) + rtn, err := wrappers.GetWrapper().SubmitOrder(tempSubmit) if err != nil { return nil, err } diff --git a/gctscript/modules/wrapper_types.go b/gctscript/modules/wrapper_types.go index f30f0337..917d9559 100644 --- a/gctscript/modules/wrapper_types.go +++ b/gctscript/modules/wrapper_types.go @@ -26,7 +26,7 @@ type Exchange interface { Ticker(exch string, pair currency.Pair, item asset.Item) (*ticker.Price, error) Pairs(exch string, enabledOnly bool, item asset.Item) (*currency.Pairs, error) QueryOrder(exch, orderid string) (*order.Detail, error) - SubmitOrder(exch string, submit *order.Submit) (*order.SubmitResponse, error) + SubmitOrder(submit *order.Submit) (*order.SubmitResponse, error) CancelOrder(exch, orderid string) (bool, error) AccountInformation(exch string) (account.Holdings, error) DepositAddress(exch string, currencyCode currency.Code) (string, error) diff --git a/gctscript/wrappers/gct/exchange/exchange.go b/gctscript/wrappers/gct/exchange/exchange.go index d1bca9e1..3827f053 100644 --- a/gctscript/wrappers/gct/exchange/exchange.go +++ b/gctscript/wrappers/gct/exchange/exchange.go @@ -89,8 +89,8 @@ func (e Exchange) QueryOrder(exch, orderID string) (*order.Detail, error) { } // SubmitOrder submit new order on exchange -func (e Exchange) SubmitOrder(exch string, submit *order.Submit) (*order.SubmitResponse, error) { - r, err := engine.Bot.OrderManager.Submit(exch, submit) +func (e Exchange) SubmitOrder(submit *order.Submit) (*order.SubmitResponse, error) { + r, err := engine.Bot.OrderManager.Submit(submit) if err != nil { return nil, err } @@ -106,13 +106,13 @@ func (e Exchange) CancelOrder(exch, orderID string) (bool, error) { } cancel := &order.Cancel{ - AccountID: orderDetails.AccountID, - OrderID: orderDetails.ID, - CurrencyPair: orderDetails.CurrencyPair, - Side: orderDetails.OrderSide, + AccountID: orderDetails.AccountID, + ID: orderDetails.ID, + Pair: orderDetails.Pair, + Side: orderDetails.Side, } - err = engine.Bot.OrderManager.Cancel(exch, cancel) + err = engine.Bot.OrderManager.Cancel(cancel) if err != nil { return false, err } diff --git a/gctscript/wrappers/gct/exchange/exchange_test.go b/gctscript/wrappers/gct/exchange/exchange_test.go index 3dd1e1b9..3971e79b 100644 --- a/gctscript/wrappers/gct/exchange/exchange_test.go +++ b/gctscript/wrappers/gct/exchange/exchange_test.go @@ -143,15 +143,16 @@ func TestExchange_SubmitOrder(t *testing.T) { } tempOrder := &order.Submit{ Pair: currency.NewPairDelimiter(pairs, delimiter), - OrderType: orderType, - OrderSide: orderSide, + Type: orderType, + Side: orderSide, TriggerPrice: 0, TargetAmount: 0, Price: orderPrice, Amount: orderAmount, ClientID: orderClientID, + Exchange: exchName, } - _, err := exchangeTest.SubmitOrder(exchName, tempOrder) + _, err := exchangeTest.SubmitOrder(tempOrder) if err != nil { t.Fatal(err) } diff --git a/gctscript/wrappers/validator/validator.go b/gctscript/wrappers/validator/validator.go index a87881eb..93645372 100644 --- a/gctscript/wrappers/validator/validator.go +++ b/gctscript/wrappers/validator/validator.go @@ -99,10 +99,10 @@ func (w Wrapper) QueryOrder(exch, _ string) (*order.Detail, error) { Exchange: exch, AccountID: "hello", ID: "1", - CurrencyPair: currency.NewPairFromString("BTCAUD"), - OrderSide: "ask", - OrderType: "limit", - OrderDate: time.Now(), + Pair: currency.NewPairFromString("BTCAUD"), + Side: "ask", + Type: "limit", + Date: time.Now(), Status: "cancelled", Price: 1, Amount: 2, @@ -126,17 +126,20 @@ func (w Wrapper) QueryOrder(exch, _ string) (*order.Detail, error) { } // SubmitOrder validator for test execution/scripts -func (w Wrapper) SubmitOrder(exch string, _ *order.Submit) (*order.SubmitResponse, error) { - if exch == exchError.String() { +func (w Wrapper) SubmitOrder(o *order.Submit) (*order.SubmitResponse, error) { + if o == nil { + return nil, errTestFailed + } + if o.Exchange == exchError.String() { return nil, errTestFailed } tempOrder := &order.SubmitResponse{ IsOrderPlaced: false, - OrderID: exch, + OrderID: o.Exchange, } - if exch == "true" { + if o.Exchange == "true" { tempOrder.IsOrderPlaced = true } diff --git a/gctscript/wrappers/validator/validator_test.go b/gctscript/wrappers/validator/validator_test.go index dd860a41..18e31899 100644 --- a/gctscript/wrappers/validator/validator_test.go +++ b/gctscript/wrappers/validator/validator_test.go @@ -147,20 +147,21 @@ func TestWrapper_SubmitOrder(t *testing.T) { tempOrder := &order.Submit{ Pair: currency.NewPairDelimiter(pairs, delimiter), - OrderType: orderType, - OrderSide: orderSide, + Type: orderType, + Side: orderSide, TriggerPrice: 0, TargetAmount: 0, Price: orderPrice, Amount: orderAmount, ClientID: orderClientID, + Exchange: "true", } - _, err := testWrapper.SubmitOrder("true", tempOrder) + _, err := testWrapper.SubmitOrder(tempOrder) if err != nil { t.Fatal(err) } - _, err = testWrapper.SubmitOrder(exchError.String(), nil) + _, err = testWrapper.SubmitOrder(nil) if err == nil { t.Fatal("expected SubmitOrder to return error with invalid name") } diff --git a/testdata/http_mock/binance/binance.json b/testdata/http_mock/binance/binance.json index c3694440..1420e153 100644 --- a/testdata/http_mock/binance/binance.json +++ b/testdata/http_mock/binance/binance.json @@ -45420,6 +45420,34 @@ } ] }, + "/api/v3/userDataStream": { + "POST": [ + { + "data": { + "listenKey": "LOLOLOLOLOLOLOLOLOLOLOLOLOLOLOLOLOLOLOLOLOLOLOLOLOLOLOLOLOLO" + }, + "queryString": "", + "bodyParams": "", + "headers": { + "X-Mbx-Apikey": [ + "" + ] + } + } + ], + "PUT": [ + { + "data": null, + "queryString": "listenKey=LOLOLOLOLOLOLOLOLOLOLOLOLOLOLOLOLOLOLOLOLOLOLOLOLOLOLOLOLOLO", + "bodyParams": "", + "headers": { + "X-Mbx-Apikey": [ + "" + ] + } + } + ] + }, "/wapi/v3/depositAddress.html": { "GET": [ {