Update BTSE RESTful API to version 2 (#349)

Update BTSE RESTful API to version 2
This commit is contained in:
Adam
2019-10-25 18:07:26 +11:00
committed by Adrian Gallagher
parent 8a0c5f95d5
commit f60051a331
5 changed files with 496 additions and 412 deletions

View File

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

View File

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

View File

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

View File

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

View File

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