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": [ {