Feature: Websocket order handling (#446)

* Initial changes, removing exchange name as an arg and puts it in the pointer struct. Adds case to ws routines

* Adds CancelAllOrders func, adds GetByExchangeAndID. Adds modify handler in routines.go

* initial poor attempts to have bitmex work with new datahandler handlers. fixes ordersides

* bitmex Completes new order

* Better bitmex handling, but not complete. Begins a gargantuan task of unifying order data structs. Sometimes an order update will contain lot's of information, so its best to be able to update all fields of our orders, rather than just an arbitrary subset. As a result, everything will be broken for the foreseeable future :glitch_crab:

* Removes old order handler which did nothing. Updates order properties for everything everywhere - now consistent. Changes order status. Adds asset type and wallet address to all order types

* Adds order updater to update only relevant fields since the object is generic, we don't know what fields are passed from what exchanges. Adds "lastupdated" field to order.Detail. Expands order cancellation for engine orders.

* Ensures that new orders are added to the ordermanager's order store. Saaa many comments. Internalises orderStore get func. Adds internalOrderID to orderdetail and adds websocket support for it

* Fixes a cancelAllOrders oopsie doopsie

* Adds potential func to update orderdetails from an orderdetail struct. Unsure if will keep.

* Begins btcmarkets implementation. Expands order "stringToOrder" funcs to allow for some more flexible string coversions. Removes order.Submit via websocket as it would cause unlimited order place issues :D

* Finishes btc markets without testing

* Adds untested ws auth func to btse

* Finises btse, fixes btcmarkets bug

* Adds coinbasepro support

* Fixes a few more fields in coinbase pro and readds the extra subs

* Begins work on coinbene. Plus theyve added a new ws connection yeee

* Wasted a bunch of time adding support to an additional websocket that isn't needed ;_; Fixed a bug in coinbasepro. Fully kitted out coinbene support. Updates order types with all fields

* Removes extra websocket connection ;_;

* Finishes gemini. Fixes order side unknown

* Adds okgroup support. Moves byte reading to another function to allow for unit testing. Updates routines to use pointers. Updates date update handling for order details

* Finishes order data for okgroup websocket, but starts the STRANGE process of converting all other websocket endpoints to be a little less silly

* Cleans up okroup websocket implementation. Fixes bug in Gemini

* Adds poloniex support. Updates ws order handling

* new bitmex support. Adds some tests now that its all in its own func. Fixes poloniex bug

* Begins work on authenticated binance websocket

* Attempts to track user data via binance websocket

* Maybe finishes Binance websocket support

* Begins adding test coverage to orders.go. Updates names of script properties to match updated

* Begins an experiment with code coverage. Fixes more rebase issues

* Completes orders coverage. Botches a few other things though. Fixes more scripting stuff

* All tests in engine package pass

* Adds some loevely routine tests

* Moves ordermanager to test Bot ordermanager
Adds lovely routine tests to ensure things that get sent to be handled the data handler are handled by the data handler by handling them

* Replaces "wsHandleData" with "wsReadData" as that's what its going to do now.

* Splits all wsHandleData into wsReadData and wsHandleData to allow for easy testing via sending []byte json examples to test proper functionality. Breaks so many tests

* Fixes majority of test issues. But data races which are tough on the engine package

* "Fixes" test by removing shutdown test. It interferes with too many things. Requires some thought

* Tests all the binance websocket points

* Adds better bitfinex websocket support.

* Adds testing for bitfinex, bitstamp and btcmarkets. Fixes websocket bugs encountered

* Adds BTSE ws tests. Fixes bugs in ws

* Adds coinbase pro tests. Fixes any issues

* Coinbene tests

* Starts to handle coinut. Runs into a problem conceptually regarding websocket roundtrip and orders. Both events need to happen without impacting eachother/racing

* Addresses a data race issue regarding websocket and bot order management submission - order submission locks at an earlier point to prevent routines.go from creating an order before order submission creates it. Updates rpcserver to use order management bot to submit orders.

* Finishes the hectic coinut testing

* Adds tests for gateio

* Fixes rebase issues. Updates tests to work without being overloaded

* Begins testing of gemini. fixes up minor issues

* ginishes gemini tests and fixes

* Adds hitbtc tests. Fixes all the many issues with hitbtc websocket

* Adds remaining tests. Increases default test channel limit again

* Begins work towards huobi tests

* Finishes huobi tests

* Fixed all mythical rebase adventures

* Begins kraken transformation

* Finishes kraken. Fixes coinbene leverage now that its changed

* Begins okgroup testing

* Adds okgroup ws tests

* Does some poloniex

* Fixes basic curreny issue by extracting to func

* Begins redesign of poloniex websocket datahandling. Completes authenticated handling, now onto unauth

* Finishes poloniex revision

* Finishes ZB additions

* Fixes data races

* Fixes rebase issues. Fixes bad kraken logic

* Fixes after reviewing code

* lint everywhere

* Fixes lingering lints

* lint

* Adds test coverage to order detail and modify updating

* Fixes linting

* Fixes huge int, fixes date tests

* Adds GetByExchange, adds test for it. Protects fakepass echange. Renames DisplayQty to DisplayQuantity. Removes verbose. Adds some websocket properties.  Updates bitmex asset type in test

* Addresses timestamps, type abbreviations, verbosity. Expands binance kline switch cases. Updates some websocket capabilities.

* Adds coverage to the stringToOrderType/Status functions introduced in PR

* Minor fixes addressing some time, error text and use of StringDataCompareInsensitive

* Introduces shiny new system which checks if there is an awaiting ID, if found, processes via wrapper method, else, goes through wsHandleData method. Removes weird locking system from wrapper/websocket data race. Updates bitfinex to properly handle websocket order requests and notifications

* Moves fakePassingExchange to test_helper. Fixes some order side implementations for trades. Botches a new error type

* Adds new error type to track and handle order classification errors separately

* Fully fleshes out ClassificationError for all instances of status conversion. Even in order trades and some wrapper functions

* Introduces common.SimpleTimeFormat for "2006-01-02 15:04:05". Fixes binance and bitfinex issues with auth endpoint use, map casting. Expands more order.ClassificationError usage. Fixes some more generic websocket response errors

* Future proofs order updating by utilising asset types. Expands testing to accomodate. Adds shiny new time type. Expands wrapper websocket functionality definitions

* minty linty

* Broken end of day code addressing basic nits on comments, returns and currency conversion

* Adds testing to btcmarkets websocket. Also updates websocket orderbook to use update instead

* Fixes fun rebase fun fun so fun

* Addresses minor nits regarding changed interface and comments

* Creates new function `GetRequestFormattedPairAndAssetType` to retrieve a currency pair and asset type based on a string. It will iterate over enabled pairs and compare them to formatted pairs and then return that pair if found.

* Fixes test

* Adds a single line to the end of the file, because that would be really bad if it wasn't there

* Updates fakepassexchange to not use params, updates test params, uses fatal in some tests where its important, updates order manager to have a rwmutex, removes some returns, improves ws key test for binance, updates properties to reflect their actual values, adds some more websocket properties

* Addresses binance switch linting

* Updates leverage property to int64

* Fixes what was broken
This commit is contained in:
Scott
2020-03-03 13:32:14 +11:00
committed by GitHub
parent 9d49184bc6
commit b686cf2e0e
142 changed files with 13867 additions and 6043 deletions

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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,
})
}

View File

@@ -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())
}

View File

@@ -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())
}

View File

@@ -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)
}
}
}

View File

@@ -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"`
}

View File

@@ -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,

View File

@@ -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
}

File diff suppressed because one or more lines are too long

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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")
}

View File

@@ -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
}

View File

@@ -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"`

View File

@@ -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)
}
}

View File

@@ -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

View File

@@ -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 {

View File

@@ -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

View File

@@ -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(&params)
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
}

View File

@@ -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())
}

View File

@@ -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())
}

File diff suppressed because one or more lines are too long

View File

@@ -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"`

View File

@@ -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,

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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
}

View File

@@ -70,7 +70,7 @@ const (
orderChange = "orderChange"
heartbeat = "heartbeat"
tick = "tick"
wsOB = "orderbook"
wsOB = "orderbookUpdate"
trade = "trade"
)

View File

@@ -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)
}
}

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)
}
}
}

View File

@@ -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"`
}

View File

@@ -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, &notification)
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)
}

View File

@@ -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
}

View File

@@ -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)
}
}
}

View File

@@ -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"`
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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)
}
}

View File

@@ -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"`

View File

@@ -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

View File

@@ -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

View File

@@ -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
}

View File

@@ -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)
}
}
}

View File

@@ -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"`
}

View File

@@ -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()

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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 := &currency.PairFormat{
Delimiter: "#",
}
b.CurrencyPairs.RequestFormat = pFmt
b.CurrencyPairs.ConfigFormat = pFmt
b.CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore)
b.CurrencyPairs.Pairs[asset.Spot] = &currency.PairStore{
Enabled: currency.Pairs{
currency.NewPair(currency.BTC, currency.USD),
},
}
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")
}
}

View File

@@ -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

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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"`
}

View File

@@ -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() {

View File

@@ -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
}

View File

@@ -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())
}

View File

@@ -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())
}

View File

@@ -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)
}
}
}

View File

@@ -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"`
}

View File

@@ -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,

View File

@@ -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
}

View File

@@ -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)
}
}

View File

@@ -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

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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)
}
}
}

View File

@@ -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"`
}

View File

@@ -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) {

View File

@@ -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
}
}

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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)
}
}

View File

@@ -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

View File

@@ -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()

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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{

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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)

File diff suppressed because one or more lines are too long

View File

@@ -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,

File diff suppressed because one or more lines are too long

View File

@@ -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,

View File

@@ -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

View File

@@ -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])
}
}

View File

@@ -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),
})
}

View File

@@ -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")
}
}

View File

@@ -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)
}

View File

@@ -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")
}
}

View File

@@ -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")
}
}

Some files were not shown because too many files have changed in this diff Show More