diff --git a/exchanges/btse/btse.go b/exchanges/btse/btse.go index 87e9e550..e1ee9a93 100644 --- a/exchanges/btse/btse.go +++ b/exchanges/btse/btse.go @@ -1,11 +1,12 @@ package btse import ( + "bytes" "errors" "fmt" + "io" "net/http" "strconv" - "strings" "time" "github.com/thrasher-corp/gocryptotrader/common" @@ -25,22 +26,23 @@ type BTSE struct { } const ( - btseAPIURL = "https://api.btse.com/v1/restapi" - btseAPIVersion = "1" + btseAPIURL = "https://api.btse.com" + btseAPIPath = "/spot/v2/" // Public endpoints - btseMarkets = "markets" - btseTrades = "trades" - btseTicker = "ticker" - btseStats = "stats" - btseTime = "time" + btseMarketOverview = "market_summary" + btseMarkets = "markets" + btseOrderbook = "orderbook" + btseTrades = "trades" + btseTicker = "ticker" + btseStats = "stats" + btseTime = "time" // Authenticated endpoints btseAccount = "account" btseOrder = "order" btsePendingOrders = "pending" btseDeleteOrder = "deleteOrder" - btseDeleteOrders = "deleteOrders" btseFills = "fills" ) @@ -140,17 +142,30 @@ func (b *BTSE) Setup(exch *config.ExchangeConfig) { } } +// GetMarketsSummary stores market summary data +func (b *BTSE) GetMarketsSummary() (*HighLevelMarketData, error) { + var m HighLevelMarketData + return &m, b.SendHTTPRequest(http.MethodGet, btseMarketOverview, &m) +} + // GetMarkets returns a list of markets available on BTSE -func (b *BTSE) GetMarkets() (*Markets, error) { - var m Markets - return &m, b.SendHTTPRequest(http.MethodGet, btseMarkets, &m) +func (b *BTSE) GetMarkets() ([]Market, error) { + var m []Market + return m, b.SendHTTPRequest(http.MethodGet, btseMarkets, &m) +} + +// FetchOrderBook gets orderbook data for a given pair +func (b *BTSE) FetchOrderBook(symbol string) (*Orderbook, error) { + var o Orderbook + endpoint := fmt.Sprintf("%s/%s", btseOrderbook, symbol) + return &o, b.SendHTTPRequest(http.MethodGet, endpoint, &o) } // GetTrades returns a list of trades for the specified symbol -func (b *BTSE) GetTrades(symbol string) (*Trades, error) { - var t Trades +func (b *BTSE) GetTrades(symbol string) ([]Trade, error) { + var t []Trade endpoint := fmt.Sprintf("%s/%s", btseTrades, symbol) - return &t, b.SendHTTPRequest(http.MethodGet, endpoint, &t) + return t, b.SendHTTPRequest(http.MethodGet, endpoint, &t) } @@ -179,24 +194,28 @@ func (b *BTSE) GetServerTime() (*ServerTime, error) { } // GetAccountBalance returns the users account balance -func (b *BTSE) GetAccountBalance() (*AccountBalance, error) { - var a AccountBalance - return &a, b.SendAuthenticatedHTTPRequest(http.MethodGet, btseAccount, nil, &a) +func (b *BTSE) GetAccountBalance() ([]CurrencyBalance, error) { + var a []CurrencyBalance + return a, b.SendAuthenticatedHTTPRequest(http.MethodGet, btseAccount, nil, &a) } // CreateOrder creates an order func (b *BTSE) CreateOrder(amount, price float64, side, orderType, symbol, timeInForce, tag string) (*string, error) { req := make(map[string]interface{}) - req["amount"] = strconv.FormatFloat(amount, 'f', -1, 64) - req["price"] = strconv.FormatFloat(price, 'f', -1, 64) - req["side"] = side - req["type"] = orderType - req["product_id"] = symbol - - if timeInForce != "" { - req["time_in_force"] = timeInForce + req["amount"] = amount + req["price"] = price + if side != "" { + req["side"] = side + } + if orderType != "" { + req["type"] = orderType + } + if symbol != "" { + req["symbol"] = symbol + } + if timeInForce != "" { + req["timeInForce"] = timeInForce } - if tag != "" { req["tag"] = tag } @@ -210,42 +229,30 @@ func (b *BTSE) CreateOrder(amount, price float64, side, orderType, symbol, timeI } // GetOrders returns all pending orders -func (b *BTSE) GetOrders(productID string) (*OpenOrders, error) { +func (b *BTSE) GetOrders(symbol string) ([]OpenOrder, error) { req := make(map[string]interface{}) - if productID != "" { - req["product_id"] = productID + if symbol != "" { + req["symbol"] = symbol } - var o OpenOrders - return &o, b.SendAuthenticatedHTTPRequest(http.MethodGet, btsePendingOrders, req, &o) + var o []OpenOrder + return o, b.SendAuthenticatedHTTPRequest(http.MethodGet, btsePendingOrders, req, &o) } // CancelExistingOrder cancels an order -func (b *BTSE) CancelExistingOrder(orderID, productID string) (*CancelOrder, error) { +func (b *BTSE) CancelExistingOrder(orderID, symbol string) (*CancelOrder, error) { var c CancelOrder req := make(map[string]interface{}) req["order_id"] = orderID - req["product_id"] = productID + req["symbol"] = symbol return &c, b.SendAuthenticatedHTTPRequest(http.MethodPost, btseDeleteOrder, req, &c) } -// CancelOrders cancels all orders -// productID optional. If product ID is sent, all orders of that specified market -// will be cancelled. If not specified, all orders of all markets will be cancelled -func (b *BTSE) CancelOrders(productID string) (*CancelOrder, error) { - var c CancelOrder - req := make(map[string]interface{}) - if productID != "" { - req["product_id"] = productID - } - return &c, b.SendAuthenticatedHTTPRequest(http.MethodPost, btseDeleteOrders, req, &c) -} - // GetFills gets all filled orders -func (b *BTSE) GetFills(orderID, productID, before, after, limit string) (*FilledOrders, error) { - if orderID != "" && productID != "" { - return nil, errors.New("orderID and productID cannot co-exist in the same query") - } else if orderID == "" && productID == "" { - return nil, errors.New("orderID OR productID must be set") +func (b *BTSE) GetFills(orderID, symbol, before, after, limit, username string) ([]FilledOrder, error) { + if orderID != "" && symbol != "" { + return nil, errors.New("orderID and symbol cannot co-exist in the same query") + } else if orderID == "" && symbol == "" { + return nil, errors.New("orderID OR symbol must be set") } req := make(map[string]interface{}) @@ -253,8 +260,8 @@ func (b *BTSE) GetFills(orderID, productID, before, after, limit string) (*Fille req["order_id"] = orderID } - if productID != "" { - req["product_id"] = productID + if symbol != "" { + req["symbol"] = symbol } if before != "" { @@ -268,15 +275,18 @@ func (b *BTSE) GetFills(orderID, productID, before, after, limit string) (*Fille if limit != "" { req["limit"] = limit } + if username != "" { + req["username"] = username + } - var o FilledOrders - return &o, b.SendAuthenticatedHTTPRequest(http.MethodPost, btseFills, req, &o) + var o []FilledOrder + return o, b.SendAuthenticatedHTTPRequest(http.MethodPost, btseFills, req, &o) } // SendHTTPRequest sends an HTTP request to the desired endpoint func (b *BTSE) SendHTTPRequest(method, endpoint string, result interface{}) error { return b.SendPayload(method, - fmt.Sprintf("%s/%s", btseAPIURL, endpoint), + btseAPIURL+btseAPIPath+endpoint, nil, nil, &result, @@ -292,27 +302,41 @@ func (b *BTSE) SendAuthenticatedHTTPRequest(method, endpoint string, req map[str if !b.AuthenticatedAPISupport { return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, b.Name) } - - payload, err := common.JSONEncode(req) - if err != nil { - return errors.New("sendAuthenticatedAPIRequest: unable to JSON request") - } - + path := btseAPIPath + endpoint headers := make(map[string]string) - headers["API-KEY"] = b.APIKey - headers["API-PASSPHRASE"] = b.APISecret - if len(payload) > 0 { - headers["Content-Type"] = "application/json" + headers["btse-api"] = b.APIKey + nonce := strconv.FormatInt(time.Now().UnixNano()/int64(time.Millisecond), 10) + headers["btse-nonce"] = nonce + var body io.Reader + var hmac []byte + var payload []byte + if len(req) != 0 { + var err error + payload, err = common.JSONEncode(req) + if err != nil { + return err + } + body = bytes.NewBuffer(payload) + hmac = common.GetHMAC( + common.HashSHA512_384, + []byte((path + nonce + string(payload))), + []byte(b.APISecret), + ) + } else { + hmac = common.GetHMAC( + common.HashSHA512_384, + []byte((path + nonce)), + []byte(b.APISecret), + ) } - - p := fmt.Sprintf("%s/%s", btseAPIURL, endpoint) + headers["btse-sign"] = common.HexEncodeToString(hmac) if b.Verbose { - log.Debugf("Sending %s request to URL %s with params %s\n", method, p, string(payload)) + log.Debugf("Sending %s request to URL %s with params %s\n", method, path, string(payload)) } return b.SendPayload(method, - p, + btseAPIURL+path, headers, - strings.NewReader(string(payload)), + body, &result, true, false, @@ -327,12 +351,19 @@ func (b *BTSE) GetFee(feeBuilder *exchange.FeeBuilder) (float64, error) { switch feeBuilder.FeeType { case exchange.CryptocurrencyTradeFee: - fee = calculateTradingFee(feeBuilder.IsMaker) + fee = calculateTradingFee(feeBuilder.IsMaker) * feeBuilder.Amount * feeBuilder.PurchasePrice case exchange.CryptocurrencyWithdrawalFee: - if feeBuilder.Pair.Base.Match(currency.BTC) { + switch feeBuilder.Pair.Base { + case currency.USDT: + fee = 1.08 + case currency.TUSD: + fee = 1.09 + case currency.BTC: fee = 0.0005 - } else if feeBuilder.Pair.Base.Match(currency.USDT) { - fee = 5 + case currency.ETH: + fee = 0.01 + case currency.LTC: + fee = 0.001 } case exchange.InternationalBankDepositFee: fee = getInternationalBankDepositFee(feeBuilder.Amount) @@ -346,7 +377,7 @@ func (b *BTSE) GetFee(feeBuilder *exchange.FeeBuilder) (float64, error) { // getOfflineTradeFee calculates the worst case-scenario trading fee func getOfflineTradeFee(price, amount float64) float64 { - return 0.0015 * price * amount + return 0.001 * price * amount } // getInternationalBankDepositFee returns international deposit fee @@ -355,7 +386,7 @@ func getOfflineTradeFee(price, amount float64) float64 { // The small deposit fee is charged in whatever currency it comes in. func getInternationalBankDepositFee(amount float64) float64 { var fee float64 - if amount <= 1000 { + if amount <= 100 { fee = amount * 0.0025 if fee < 3 { return 3 @@ -367,7 +398,7 @@ func getInternationalBankDepositFee(amount float64) float64 { // getInternationalBankWithdrawalFee returns international withdrawal fee // 0.1% (min25 USD) func getInternationalBankWithdrawalFee(amount float64) float64 { - fee := amount * 0.001 + fee := amount * 0.0009 if fee < 25 { return 25 @@ -380,7 +411,7 @@ func getInternationalBankWithdrawalFee(amount float64) float64 { func calculateTradingFee(isMaker bool) float64 { fee := 0.00050 if !isMaker { - fee = 0.0015 + fee = 0.001 } return fee } diff --git a/exchanges/btse/btse_test.go b/exchanges/btse/btse_test.go index 9d641eb8..93439bfa 100644 --- a/exchanges/btse/btse_test.go +++ b/exchanges/btse/btse_test.go @@ -1,12 +1,13 @@ package btse import ( + "os" "testing" - "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + log "github.com/thrasher-corp/gocryptotrader/logger" ) // Please supply your own keys here to do better tests @@ -18,16 +19,13 @@ const ( var b BTSE -func TestSetDefaults(t *testing.T) { +func TestMain(m *testing.M) { b.SetDefaults() -} - -func TestSetup(t *testing.T) { cfg := config.GetConfig() cfg.LoadConfig("../../testdata/configtest.json") btseConfig, err := cfg.GetExchangeConfig("BTSE") if err != nil { - t.Error("Test Failed - BTSE Setup() init error") + log.Fatal("BTSE Setup() init error", err) } btseConfig.AuthenticatedAPISupport = true @@ -35,104 +33,149 @@ func TestSetup(t *testing.T) { btseConfig.APISecret = apiSecret b.Setup(&btseConfig) + os.Exit(m.Run()) +} + +func areTestAPIKeysSet() bool { + if b.APIKey != "" && b.APIKey != "Key" && + b.APISecret != "" && b.APISecret != "Secret" { + return true + } + return false +} + +func TestGetMarketsSummary(t *testing.T) { + t.Parallel() + _, err := b.GetMarketsSummary() + if err != nil { + t.Error(err) + } } func TestGetMarkets(t *testing.T) { - b.SetDefaults() + t.Parallel() _, err := b.GetMarkets() if err != nil { - t.Fatalf("Test failed. Err: %s", err) + t.Error(err) + } +} + +func TestFetchOrderBook(t *testing.T) { + t.Parallel() + _, err := b.FetchOrderBook("BTC-USD") + if err != nil { + t.Error(err) } } func TestGetTrades(t *testing.T) { - b.SetDefaults() + t.Parallel() _, err := b.GetTrades("BTC-USD") if err != nil { - t.Fatalf("Test failed. Err: %s", err) + t.Error(err) } } func TestGetTicker(t *testing.T) { - b.SetDefaults() + t.Parallel() _, err := b.GetTicker("BTC-USD") if err != nil { - t.Fatalf("Test failed. Err: %s", err) + t.Error(err) } } func TestGetMarketStatistics(t *testing.T) { - b.SetDefaults() + t.Parallel() _, err := b.GetMarketStatistics("BTC-USD") if err != nil { - t.Fatalf("Test failed. Err: %s", err) + t.Error(err) } } func TestGetServerTime(t *testing.T) { - b.SetDefaults() + t.Parallel() _, err := b.GetServerTime() if err != nil { - t.Fatalf("Test failed. Err: %s", err) + t.Error(err) } } func TestGetAccount(t *testing.T) { - b.SetDefaults() - TestSetup(t) - + t.Parallel() + if !areTestAPIKeysSet() { + t.Skip("API keys not set, skipping test") + } _, err := b.GetAccountBalance() - if areTestAPIKeysSet() && err != nil { - t.Errorf("Could not get account balance: %s", err) - } else if !areTestAPIKeysSet() && err == nil { - t.Error("Expecting an error when no keys are set") + if err != nil { + t.Error(err) } } func TestGetFills(t *testing.T) { - b.SetDefaults() - TestSetup(t) - - _, err := b.GetFills("", "BTC-USD", "", "", "") - if areTestAPIKeysSet() && err != nil { - t.Errorf("Could not get fills: %s", err) - } else if !areTestAPIKeysSet() && err == nil { - t.Error("Expecting an error when no keys are set") + t.Parallel() + if !areTestAPIKeysSet() { + t.Skip("API keys not set, skipping test") + } + _, err := b.GetFills("", "BTC-USD", "", "", "", "") + if err != nil { + t.Error(err) } } -func TestGetActiveOrders(t *testing.T) { - b.SetDefaults() - TestSetup(t) +func TestCreateOrder(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() || !canManipulateRealOrders { + t.Skip("skipping test, either api keys or manipulaterealorders isnt set correctly") + } + _, err := b.CreateOrder(4.5, 3.4, "buy", "limit", "BTC-USD", "", "") + if err != nil { + t.Error(err) + } +} +func TestGetOrders(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.Skip("API keys not set, skipping test") + } + _, err := b.GetOrders("") + if err != nil { + t.Error(err) + } +} + +func TestGetActiveOrders(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.Skip("API keys not set, skipping test") + } var getOrdersRequest = exchange.GetOrdersRequest{ OrderType: exchange.AnyOrderType, } _, err := b.GetActiveOrders(&getOrdersRequest) - if areTestAPIKeysSet() && err != nil { - t.Errorf("Could not get open orders: %s", err) - } else if !areTestAPIKeysSet() && err == nil { - t.Error("Expecting an error when no keys are set") + if err != nil { + t.Error(err) } } func TestGetOrderHistory(t *testing.T) { - b.SetDefaults() - TestSetup(t) + t.Parallel() + if !areTestAPIKeysSet() { + t.Skip("API keys not set, skipping test") + } var getOrdersRequest = exchange.GetOrdersRequest{ OrderType: exchange.AnyOrderType, } - _, err := b.GetOrderHistory(&getOrdersRequest) - if err != common.ErrFunctionNotSupported { - t.Fatal("Test failed. Expected different result") + if err != nil { + t.Error(err) } } func TestFormatWithdrawPermissions(t *testing.T) { - b.SetDefaults() + t.Parallel() expected := exchange.NoAPIWithdrawalMethodsText actual := b.FormatWithdrawPermissions() if actual != expected { @@ -143,10 +186,11 @@ func TestFormatWithdrawPermissions(t *testing.T) { // TestGetFeeByTypeOfflineTradeFee logic test func TestGetFeeByTypeOfflineTradeFee(t *testing.T) { feeBuilder := &exchange.FeeBuilder{ - FeeType: exchange.CryptocurrencyTradeFee, - Pair: currency.NewPair(currency.BTC, currency.USD), - IsMaker: true, - Amount: 1000, + FeeType: exchange.CryptocurrencyTradeFee, + Pair: currency.NewPair(currency.BTC, currency.USD), + IsMaker: true, + Amount: 1, + PurchasePrice: 1000, } b.GetFeeByType(feeBuilder) @@ -162,60 +206,60 @@ func TestGetFeeByTypeOfflineTradeFee(t *testing.T) { } func TestGetFee(t *testing.T) { - b.SetDefaults() - TestSetup(t) + t.Parallel() feeBuilder := &exchange.FeeBuilder{ - FeeType: exchange.CryptocurrencyTradeFee, - Pair: currency.NewPair(currency.BTC, currency.USD), - IsMaker: true, - Amount: 1000, + FeeType: exchange.CryptocurrencyTradeFee, + Pair: currency.NewPair(currency.BTC, currency.USD), + IsMaker: true, + Amount: 1, + PurchasePrice: 1000, } - if resp, err := b.GetFee(feeBuilder); resp != 0.00050 || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", 0.00050, resp) + if resp, err := b.GetFee(feeBuilder); resp != 0.500000 || err != nil { + t.Errorf("GetFee() error. Expected: %f, Received: %f", 0.500000, resp) t.Error(err) } feeBuilder.IsMaker = false - if resp, err := b.GetFee(feeBuilder); resp != 0.0015 || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", 0.0015, resp) + if resp, err := b.GetFee(feeBuilder); resp != 1.00000 || err != nil { + t.Errorf("GetFee() error. Expected: %f, Received: %f", 1.00000, resp) t.Error(err) } feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee if resp, err := b.GetFee(feeBuilder); resp != 0.0005 || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", 0.0005, resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", 0.0005, resp) t.Error(err) } feeBuilder.Pair.Base = currency.USDT - if resp, err := b.GetFee(feeBuilder); resp != float64(5) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(5), resp) + if resp, err := b.GetFee(feeBuilder); resp != 1.080000 || err != nil { + t.Errorf("GetFee() error. Expected: %f, Received: %f", 1.080000, resp) t.Error(err) } feeBuilder.FeeType = exchange.InternationalBankDepositFee if resp, err := b.GetFee(feeBuilder); resp != float64(3) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(3), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(3), resp) t.Error(err) } feeBuilder.Amount = 1000000 if resp, err := b.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee - if resp, err := b.GetFee(feeBuilder); resp != float64(1000) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(1000), resp) + if resp, err := b.GetFee(feeBuilder); resp != float64(900) || err != nil { + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(900), resp) t.Error(err) } feeBuilder.Amount = 1000 if resp, err := b.GetFee(feeBuilder); resp != float64(25) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(25), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(25), resp) t.Error(err) } } @@ -224,28 +268,17 @@ func TestParseOrderTime(t *testing.T) { expected := int64(1534794360) actual := parseOrderTime("2018-08-20 19:20:46").Unix() if expected != actual { - t.Errorf("Test Failed. TestParseOrderTime expected: %d, got %d", expected, actual) + t.Errorf("TestParseOrderTime expected: %d, got %d", expected, actual) } } -func areTestAPIKeysSet() bool { - if b.APIKey != "" && b.APIKey != "Key" && - b.APISecret != "" && b.APISecret != "Secret" { - return true - } - return false -} - // Any tests below this line have the ability to impact your orders on the exchange. Enable canManipulateRealOrders to run them // ---------------------------------------------------------------------------------------------------------------------------- func TestSubmitOrder(t *testing.T) { - b.SetDefaults() - TestSetup(t) - - if areTestAPIKeysSet() && !canManipulateRealOrders { - t.Skip("API keys set, canManipulateRealOrders false, skipping test") + t.Parallel() + if !areTestAPIKeysSet() || !canManipulateRealOrders { + t.Skip("skipping test, either api keys or manipulaterealorders isnt set correctly") } - var p = currency.Pair{ Delimiter: "", Base: currency.BTC, @@ -260,13 +293,10 @@ func TestSubmitOrder(t *testing.T) { } func TestCancelExchangeOrder(t *testing.T) { - b.SetDefaults() - TestSetup(t) - - if areTestAPIKeysSet() && !canManipulateRealOrders { - t.Skip("API keys set, canManipulateRealOrders false, skipping test") + t.Parallel() + if !areTestAPIKeysSet() || !canManipulateRealOrders { + t.Skip("skipping test, either api keys or manipulaterealorders isnt set correctly") } - currencyPair := currency.NewPairWithDelimiter(currency.BTC.String(), currency.USD.String(), "-") @@ -277,24 +307,17 @@ func TestCancelExchangeOrder(t *testing.T) { AccountID: "1", CurrencyPair: currencyPair, } - err := b.CancelOrder(orderCancellation) - if !areTestAPIKeysSet() && err == nil { - t.Error("Expecting an error when no keys are set") - } - if areTestAPIKeysSet() && err != nil { - t.Errorf("Could not cancel orders: %v", err) + if err != nil { + t.Error(err) } } func TestCancelAllExchangeOrders(t *testing.T) { - b.SetDefaults() - TestSetup(t) - - if areTestAPIKeysSet() && !canManipulateRealOrders { - t.Skip("API keys set, canManipulateRealOrders false, skipping test") + t.Parallel() + if !areTestAPIKeysSet() || !canManipulateRealOrders { + t.Skip("skipping test, either api keys or manipulaterealorders isnt set correctly") } - currencyPair := currency.NewPairWithDelimiter(currency.BTC.String(), currency.USD.String(), "-") @@ -305,16 +328,11 @@ func TestCancelAllExchangeOrders(t *testing.T) { AccountID: "1", CurrencyPair: currencyPair, } - resp, err := b.CancelAllOrders(orderCancellation) - if !areTestAPIKeysSet() && err == nil { - t.Error("Expecting an error when no keys are set") - } - if areTestAPIKeysSet() && err != nil { + if err != nil { t.Errorf("Could not cancel orders: %v", err) } - if len(resp.OrderStatus) > 0 { t.Errorf("%v orders failed to cancel", len(resp.OrderStatus)) } diff --git a/exchanges/btse/btse_types.go b/exchanges/btse/btse_types.go index 6ac5781c..0914e6f4 100644 --- a/exchanges/btse/btse_types.go +++ b/exchanges/btse/btse_types.go @@ -2,21 +2,33 @@ package btse import "time" -// Market stores market data -type Market struct { - Symbol string `json:"symbol"` - BaseCurrency string `json:"base_currency"` - QuoteCurrency string `json:"quote_currency"` - BaseMinSize float64 `json:"base_min_size"` - BaseMaxSize float64 `json:"base_max_size"` - BaseIncremementSize float64 `json:"base_increment_size"` - QuoteMinPrice float64 `json:"quote_min_price"` - QuoteIncrement float64 `json:"quote_increment"` - Status string `json:"status"` +// OverviewData stores market overview data +type OverviewData struct { + High24Hr float64 `json:"high24hr,string"` + HighestBid float64 `json:"highestbid,string"` + Last float64 `json:"last,string"` + Low24Hr float64 `json:"low24hr,string"` + LowestAsk float64 `json:"lowest_ask,string"` + PercentageChange float64 `json:"percent_change,string"` + Volume float64 `json:"volume,string"` } -// Markets stores an array of market data -type Markets []Market +// HighLevelMarketData stores market overview data +type HighLevelMarketData map[string]OverviewData + +// Market stores market data +type Market struct { + Symbol string `json:"symbol"` + ID string `json:"id"` + BaseCurrency string `json:"base_currency"` + QuoteCurrency string `json:"quote_currency"` + BaseMinSize float64 `json:"base_min_size"` + BaseMaxSize float64 `json:"base_max_size"` + BaseIncrementSize float64 `json:"base_increment_size"` + QuoteMinPrice float64 `json:"quote_min_price"` + QuoteIncrement float64 `json:"quote_increment"` + Status string `json:"status"` +} // Trade stores trade data type Trade struct { @@ -28,8 +40,19 @@ type Trade struct { Type string `json:"type"` } -// Trades stores an array of trade data -type Trades []Trade +// QuoteData stores quote data +type QuoteData struct { + Price float64 `json:"price,string"` + Size float64 `json:"size,string"` +} + +// Orderbook stores orderbook info +type Orderbook struct { + BuyQuote []QuoteData `json:"buyQuote"` + SellQuote []QuoteData `json:"sellQuote"` + Symbol string `json:"symbol"` + Timestamp int64 `json:"timestamp"` +} // Ticker stores the ticker data type Ticker struct { @@ -64,9 +87,6 @@ type CurrencyBalance struct { Available float64 `json:"available,string"` } -// AccountBalance stores an array of currency balances -type AccountBalance []CurrencyBalance - // Order stores the order info type Order struct { ID string `json:"id"` @@ -75,7 +95,7 @@ type Order struct { Price float64 `json:"price"` Amount float64 `json:"amount"` Tag string `json:"tag"` - ProductID string `json:"product_id"` + Symbol string `json:"symbol"` CreatedAt string `json:"created_at"` } @@ -85,9 +105,6 @@ type OpenOrder struct { Status string `json:"status"` } -// OpenOrders stores an array of orders -type OpenOrders []OpenOrder - // CancelOrder stores the cancel order response data type CancelOrder struct { Code int `json:"code"` @@ -103,35 +120,43 @@ type FilledOrder struct { Tag string `json:"tag"` ID int64 `json:"id"` TradeID string `json:"trade_id"` - ProductID string `json:"product_id"` + Symbol string `json:"symbol"` OrderID string `json:"order_id"` CreatedAt string `json:"created_at"` } -// FilledOrders stores an array of filled orders -type FilledOrders []FilledOrder - -type websocketSubscribe struct { - Type string `json:"type"` - Channels []websocketChannel `json:"channels"` +type wsSub struct { + Operation string `json:"op"` + Arguments []string `json:"args"` } -type websocketChannel struct { - Name string `json:"name"` - ProductIDs []string `json:"product_ids"` +type wsQuoteData struct { + Total string `json:"cumulativeTotal"` + Price string `json:"price"` + Size string `json:"size"` } -type wsTicker struct { - BestAsk float64 `json:"best_ask,string"` - BestBids float64 `json:"best_bid,string"` - LastSize float64 `json:"last_size,string"` - Price interface{} `json:"price"` - ProductID string `json:"product_id"` +type wsOBData struct { + Currency string `json:"currency"` + BuyQuote []wsQuoteData `json:"buyQuote"` + SellQuote []wsQuoteData `json:"sellQuote"` } -type websocketOrderbookSnapshot struct { - ProductID string `json:"product_id"` - Type string `json:"type"` - Bids [][]interface{} `json:"bids"` - Asks [][]interface{} `json:"asks"` +type wsOrderBook struct { + Topic string `json:"topic"` + Data wsOBData `json:"data"` +} + +type wsTradeData struct { + Amount float64 `json:"amount"` + Gain int64 `json:"gain"` + Newest int64 `json:"newest"` + Price float64 `json:"price"` + ID int64 `json:"serialId"` + TransactionTime int64 `json:"transactionUnixTime"` +} + +type wsTradeHistory struct { + Topic string `json:"topic"` + Data []wsTradeData `json:"data"` } diff --git a/exchanges/btse/btse_websocket.go b/exchanges/btse/btse_websocket.go index 9b0d6fbb..0e8f3528 100644 --- a/exchanges/btse/btse_websocket.go +++ b/exchanges/btse/btse_websocket.go @@ -2,6 +2,7 @@ package btse import ( "errors" + "fmt" "net/http" "strconv" "strings" @@ -10,13 +11,15 @@ import ( "github.com/gorilla/websocket" "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/currency" + exchange "github.com/thrasher-corp/gocryptotrader/exchanges" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" log "github.com/thrasher-corp/gocryptotrader/logger" ) const ( - btseWebsocket = "wss://ws.btse.com/api/ws-feed" + btseWebsocket = "wss://ws.btse.com/spotWS" + btseWebsocketTimer = 57 * time.Second ) // WsConnect connects the websocket client @@ -29,7 +32,7 @@ func (b *BTSE) WsConnect() error { if err != nil { return err } - + go b.Pinger() go b.WsHandleData() b.GenerateDefaultSubscriptions() @@ -57,132 +60,106 @@ func (b *BTSE) WsHandleData() { } b.Websocket.TrafficAlert <- struct{}{} - type MsgType struct { - Type string `json:"type"` - ProductID string `json:"product_id"` - } - - if strings.Contains(string(resp.Raw), "Connected. Welcome to BTSE!") { - if b.Verbose { - log.Debugf("%s websocket client successfully connected to %s", - b.Name, b.Websocket.GetWebsocketURL()) - } - continue - } - - msgType := MsgType{} - err = common.JSONDecode(resp.Raw, &msgType) + type Result map[string]interface{} + result := Result{} + err = common.JSONDecode(resp.Raw, &result) if err != nil { b.Websocket.DataHandler <- err continue } - switch msgType.Type { - case "ticker": - var t wsTicker + switch { + case strings.Contains(result["topic"].(string), "tradeHistory"): + log.Warnf("%s: Buy/Sell side functionality is broken for this exchange currently! 'gain' has no correlation with buy side or sell side", b.Name) + var tradeHistory wsTradeHistory + err = common.JSONDecode(resp.Raw, &tradeHistory) + if err != nil { + b.Websocket.DataHandler <- err + continue + } + for x := range tradeHistory.Data { + side := exchange.BuyOrderSide.ToString() + if tradeHistory.Data[x].Gain == -1 { + side = exchange.SellOrderSide.ToString() + } + b.Websocket.DataHandler <- wshandler.TradeData{ + Timestamp: time.Unix(tradeHistory.Data[x].TransactionTime, 0), + CurrencyPair: currency.NewPairFromString(strings.Replace(tradeHistory.Topic, "tradeHistory", "", 1)), + AssetType: orderbook.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 = common.JSONDecode(resp.Raw, &t) if err != nil { b.Websocket.DataHandler <- err continue } - p := strings.Replace(t.Price.(string), ",", "", -1) - price, err := strconv.ParseFloat(p, 64) - if err != nil { - b.Websocket.DataHandler <- err - continue - } - - b.Websocket.DataHandler <- wshandler.TickerData{ - Timestamp: time.Now(), - Pair: currency.NewPairDelimiter(t.ProductID, "-"), - AssetType: orderbook.Spot, - Exchange: b.GetName(), - OpenPrice: price, - } - case "snapshot": - snapshot := websocketOrderbookSnapshot{} - err := common.JSONDecode(resp.Raw, &snapshot) - if err != nil { - b.Websocket.DataHandler <- err - continue - } - - err = b.wsProcessSnapshot(&snapshot) + var price, amount float64 + var asks, bids []orderbook.Item + 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 + } + asks = append(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 + } + bids = append(bids, orderbook.Item{Price: price, Amount: amount}) + } + var newOB orderbook.Base + newOB.Asks = asks + newOB.Bids = bids + newOB.AssetType = orderbook.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, true) if err != nil { b.Websocket.DataHandler <- err continue } + b.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{Pair: newOB.Pair, + Asset: orderbook.Spot, + Exchange: b.Name} + default: + log.Warnf("%s: unhandled websocket response: %s", b.Name, resp.Raw) } } } } -// ProcessSnapshot processes the initial orderbook snap shot -func (b *BTSE) wsProcessSnapshot(snapshot *websocketOrderbookSnapshot) error { - var base orderbook.Base - for i := range snapshot.Bids { - p := strings.Replace(snapshot.Bids[i][0].(string), ",", "", -1) - price, err := strconv.ParseFloat(p, 64) - if err != nil { - return err - } - - a := strings.Replace(snapshot.Bids[i][1].(string), ",", "", -1) - amount, err := strconv.ParseFloat(a, 64) - if err != nil { - return err - } - - base.Bids = append(base.Bids, - orderbook.Item{Price: price, Amount: amount}) - } - - for i := range snapshot.Asks { - p := strings.Replace(snapshot.Asks[i][0].(string), ",", "", -1) - price, err := strconv.ParseFloat(p, 64) - if err != nil { - return err - } - - a := strings.Replace(snapshot.Asks[i][1].(string), ",", "", -1) - amount, err := strconv.ParseFloat(a, 64) - if err != nil { - return err - } - - base.Asks = append(base.Asks, - orderbook.Item{Price: price, Amount: amount}) - } - - p := currency.NewPairDelimiter(snapshot.ProductID, "-") - base.AssetType = orderbook.Spot - base.Pair = p - base.LastUpdated = time.Now() - base.ExchangeName = b.Name - - err := b.Websocket.Orderbook.LoadSnapshot(&base, true) - if err != nil { - return err - } - - b.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{ - Pair: p, - Asset: orderbook.Spot, - Exchange: b.GetName(), - } - - return nil -} - // GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions() func (b *BTSE) GenerateDefaultSubscriptions() { - var channels = []string{"snapshot", "ticker"} - enabledCurrencies := b.GetEnabledCurrencies() + var channels = []string{"orderBookApi:%s_0", "tradeHistory:%s"} var subscriptions []wshandler.WebsocketChannelSubscription for i := range channels { - for j := range enabledCurrencies { + for j := range b.EnabledPairs { subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{ - Channel: channels[i], - Currency: enabledCurrencies[j], + Channel: fmt.Sprintf(channels[i], b.EnabledPairs[j]), + Currency: b.EnabledPairs[j], }) } } @@ -191,28 +168,32 @@ func (b *BTSE) GenerateDefaultSubscriptions() { // Subscribe sends a websocket message to receive data from the channel func (b *BTSE) Subscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error { - subscribe := websocketSubscribe{ - Type: "subscribe", - Channels: []websocketChannel{ - { - Name: channelToSubscribe.Channel, - ProductIDs: []string{channelToSubscribe.Currency.String()}, - }, - }, - } - return b.WebsocketConn.SendMessage(subscribe) + var sub wsSub + sub.Operation = "subscribe" + sub.Arguments = []string{channelToSubscribe.Channel} + return b.WebsocketConn.SendMessage(sub) } // Unsubscribe sends a websocket message to stop receiving data from the channel func (b *BTSE) Unsubscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error { - subscribe := websocketSubscribe{ - Type: "unsubscribe", - Channels: []websocketChannel{ - { - Name: channelToSubscribe.Channel, - ProductIDs: []string{channelToSubscribe.Currency.String()}, - }, - }, - } - return b.WebsocketConn.SendMessage(subscribe) + var unSub wsSub + unSub.Operation = "unsubscribe" + unSub.Arguments = []string{channelToSubscribe.Channel} + return b.WebsocketConn.SendMessage(unSub) +} + +// Pinger pings +func (b *BTSE) Pinger() { + ticker := time.NewTicker(btseWebsocketTimer) + + for { + select { + case <-b.Websocket.ShutdownC: + ticker.Stop() + return + + case <-ticker.C: + b.WebsocketConn.Connection.WriteMessage(websocket.PingMessage, nil) + } + } } diff --git a/exchanges/btse/btse_wrapper.go b/exchanges/btse/btse_wrapper.go index f1489b4c..6dccba73 100644 --- a/exchanges/btse/btse_wrapper.go +++ b/exchanges/btse/btse_wrapper.go @@ -33,16 +33,16 @@ func (b *BTSE) Run() { log.Debugf("%s %d currencies enabled: %s.\n", b.GetName(), len(b.EnabledPairs), b.EnabledPairs) } - markets, err := b.GetMarkets() + m, err := b.GetMarkets() if err != nil { log.Errorf("%s failed to get trading pairs. Err: %s", b.Name, err) } else { var currencies []string - for _, m := range *markets { - if m.Status != "active" { + for x := range m { + if m[x].Status != "active" { continue } - currencies = append(currencies, m.Symbol) + currencies = append(currencies, m[x].Symbol) } err = b.UpdateCurrencies(currency.NewPairsFromStrings(currencies), false, @@ -104,7 +104,29 @@ func (b *BTSE) GetOrderbookEx(p currency.Pair, assetType string) (orderbook.Base // UpdateOrderbook updates and returns the orderbook for a currency pair func (b *BTSE) UpdateOrderbook(p currency.Pair, assetType string) (orderbook.Base, error) { - return orderbook.Base{}, common.ErrFunctionNotSupported + var resp orderbook.Base + a, err := b.FetchOrderBook(exchange.FormatExchangeCurrency(b.Name, p).String()) + if err != nil { + return resp, err + } + for x := range a.BuyQuote { + resp.Bids = append(resp.Bids, orderbook.Item{ + Price: a.BuyQuote[x].Price, + Amount: a.BuyQuote[x].Size}) + } + for x := range a.SellQuote { + resp.Asks = append(resp.Asks, orderbook.Item{ + Price: a.SellQuote[x].Price, + Amount: a.SellQuote[x].Size}) + } + resp.Pair = p + resp.ExchangeName = b.Name + resp.AssetType = assetType + err = resp.Process() + if err != nil { + return resp, err + } + return orderbook.Get(b.Name, p, assetType) } // GetAccountInfo retrieves balances for all enabled currencies for the @@ -117,12 +139,12 @@ func (b *BTSE) GetAccountInfo() (exchange.AccountInfo, error) { } var currencies []exchange.AccountCurrencyInfo - for _, b := range *balance { + for b := range balance { currencies = append(currencies, exchange.AccountCurrencyInfo{ - CurrencyName: currency.NewCode(b.Currency), - TotalValue: b.Total, - Hold: b.Available, + CurrencyName: currency.NewCode(balance[b].Currency), + TotalValue: balance[b].Total, + Hold: balance[b].Available, }, ) } @@ -150,7 +172,7 @@ func (b *BTSE) GetExchangeHistory(p currency.Pair, assetType string) ([]exchange func (b *BTSE) SubmitOrder(p currency.Pair, side exchange.OrderSide, orderType exchange.OrderType, amount, price float64, clientID string) (exchange.SubmitOrderResponse, error) { var resp exchange.SubmitOrderResponse r, err := b.CreateOrder(amount, price, side.ToString(), - orderType.ToString(), exchange.FormatExchangeCurrency(b.Name, p).String(), "GTC", clientID) + orderType.ToString(), exchange.FormatExchangeCurrency(b.Name, p).String(), "", clientID) if err != nil { return resp, err } @@ -192,19 +214,30 @@ func (b *BTSE) CancelOrder(order *exchange.OrderCancellation) error { // If not specified, all orders of all markets will be cancelled func (b *BTSE) CancelAllOrders(orderCancellation *exchange.OrderCancellation) (exchange.CancelAllOrdersResponse, error) { var resp exchange.CancelAllOrdersResponse - r, err := b.CancelOrders(exchange.FormatExchangeCurrency(b.Name, - orderCancellation.CurrencyPair).String()) + a, err := b.GetMarkets() if err != nil { return resp, err } - - switch r.Code { - case -1: - return resp, errors.New("order cancellation unsuccessful") - case 4: - return resp, errors.New("order cancellation timeout") + for x := range a { + strPair := exchange.FormatExchangeCurrency(b.Name, orderCancellation.CurrencyPair).String() + checkPair := currency.NewPairWithDelimiter(a[x].BaseCurrency, a[x].QuoteCurrency, b.RequestCurrencyPairFormat.Delimiter).String() + if strPair != "" && strPair != checkPair { + continue + } else { + orders, err := b.GetOrders(checkPair) + if err != nil { + return resp, err + } + for y := range orders { + success := "Order Cancelled" + _, err = b.CancelExistingOrder(orders[y].Order.ID, checkPair) + if err != nil { + success = "Order Cancellation Failed" + } + resp.OrderStatus[orders[y].Order.ID] = success + } + } } - return resp, nil } @@ -216,48 +249,46 @@ func (b *BTSE) GetOrderInfo(orderID string) (exchange.OrderDetail, error) { } var od exchange.OrderDetail - if len(*o) == 0 { + if len(o) == 0 { return od, errors.New("no orders found") } - for i := range *o { - o := (*o)[i] - if o.ID != orderID { + for i := range o { + if o[i].ID != orderID { continue } var side = exchange.BuyOrderSide - if strings.EqualFold(o.Side, exchange.AskOrderSide.ToString()) { + if strings.EqualFold(o[i].Side, exchange.AskOrderSide.ToString()) { side = exchange.SellOrderSide } - od.CurrencyPair = currency.NewPairDelimiter(o.ProductID, + od.CurrencyPair = currency.NewPairDelimiter(o[i].Symbol, b.ConfigCurrencyPairFormat.Delimiter) od.Exchange = b.Name - od.Amount = o.Amount - od.ID = o.ID - od.OrderDate = parseOrderTime(o.CreatedAt) + od.Amount = o[i].Amount + od.ID = o[i].ID + od.OrderDate = parseOrderTime(o[i].CreatedAt) od.OrderSide = side - od.OrderType = exchange.OrderType(strings.ToUpper(o.Type)) - od.Price = o.Price - od.Status = o.Status + od.OrderType = exchange.OrderType(strings.ToUpper(o[i].Type)) + od.Price = o[i].Price + od.Status = o[i].Status - fills, err := b.GetFills(orderID, "", "", "", "") + fills, err := b.GetFills(orderID, "", "", "", "", "") if err != nil { return od, fmt.Errorf("unable to get order fills for orderID %s", orderID) } - for i := range *fills { - f := (*fills)[i] - createdAt, _ := time.Parse(time.RFC3339, f.CreatedAt) + for i := range fills { + createdAt, _ := time.Parse(time.RFC3339, fills[i].CreatedAt) od.Trades = append(od.Trades, exchange.TradeHistory{ Timestamp: createdAt, - TID: f.ID, - Price: f.Price, - Amount: f.Amount, + TID: fills[i].ID, + Price: fills[i].Price, + Amount: fills[i].Amount, Exchange: b.Name, - Type: exchange.OrderSide(f.Side).ToString(), - Fee: f.Fee, + Type: fills[i].Side, + Fee: fills[i].Fee, }) } } @@ -300,43 +331,41 @@ func (b *BTSE) GetActiveOrders(getOrdersRequest *exchange.GetOrdersRequest) ([]e } var orders []exchange.OrderDetail - for i := range *resp { - order := (*resp)[i] + for i := range resp { var side = exchange.BuyOrderSide - if strings.EqualFold(order.Side, exchange.AskOrderSide.ToString()) { + if strings.EqualFold(resp[i].Side, exchange.AskOrderSide.ToString()) { side = exchange.SellOrderSide } openOrder := exchange.OrderDetail{ - CurrencyPair: currency.NewPairDelimiter(order.ProductID, + CurrencyPair: currency.NewPairDelimiter(resp[i].Symbol, b.ConfigCurrencyPairFormat.Delimiter), Exchange: b.Name, - Amount: order.Amount, - ID: order.ID, - OrderDate: parseOrderTime(order.CreatedAt), + Amount: resp[i].Amount, + ID: resp[i].ID, + OrderDate: parseOrderTime(resp[i].CreatedAt), OrderSide: side, - OrderType: exchange.OrderType(strings.ToUpper(order.Type)), - Price: order.Price, - Status: order.Status, + OrderType: exchange.OrderType(strings.ToUpper(resp[i].Type)), + Price: resp[i].Price, + Status: resp[i].Status, } - fills, err := b.GetFills(order.ID, "", "", "", "") + fills, err := b.GetFills(resp[i].ID, "", "", "", "", "") if err != nil { - log.Errorf("unable to get order fills for orderID %s", order.ID) + log.Errorf("%s: unable to get order fills for orderID %s", b.Name, resp[i].ID) continue } - for i := range *fills { - f := (*fills)[i] - createdAt, _ := time.Parse(time.RFC3339, f.CreatedAt) + for i := range fills { + createdAt, _ := time.Parse(time.RFC3339, fills[i].CreatedAt) openOrder.Trades = append(openOrder.Trades, exchange.TradeHistory{ Timestamp: createdAt, - TID: f.ID, - Price: f.Price, - Amount: f.Amount, + TID: fills[i].ID, + Price: fills[i].Price, + Amount: fills[i].Amount, Exchange: b.Name, - Type: exchange.OrderSide(f.Side).ToString(), - Fee: f.Fee, + Type: fills[i].Side, + Fee: fills[i].Fee, }) } orders = append(orders, openOrder)