Exchange: BTSE API 3.2 upgrade (#548)

* BTSE: bump API version to 3.2

* BTSE: added AccountFee endpoint

* BTSE: ratelimit, modified GetFee() to use GetFeeInformation, CreateWalletAddress support

* BTSE: gofmt

* BTSE: renamed ratelimits to ratelimit

* BTSE: comments on exported methods, reworked const

* BTSE: remove verbose

* BTSE: increased test coverage

* BTSE: removed futures test

* BTSE: comments, correct types, moon

* Add support for futures ticker/orderbook, pass data from OHLCV to append because data is important :D

* BTSE: update futures test pair

* BTSE: updated creatorder test to use negative numbers

* BTSE: updated test wording

* BTSE: no BTSEx anymore

* BTSE: use GetEnabledPairs

* BTSE: updated order structu

* BTSE: goimport test package

* BTSE: added orderIntToType method

* BTSE: added extra params to Trade/OpenOrders, kline format method added

* BTSE: CreateOrder and IndexOrderPeg updates

* removed binary

* BTSE: type fixes for orderid, comments

* BTSE: remove float tos tring conversion correct casing

* BTSE: updated return types

* BTSE: return slice

* BTSE: update type to string, fixed comment on Price(), removed verbose flag

* BTSE: use FormatExchangeCurrency()

* BTSE: status -> string

* BTSE: added withinLimit method to confirm order is within valid limits

* BTSE: gofmt

* BTSE: updated comment

* BTSE: comment update

* BTSE: init map for cancelallexcahgneorders

* BTSE: updated json structs for trade history, reworked withinlimits and ordersizelimits to use sync.map

* BTSE: test other currencies to confirm matching values for incerement

* BTSE: comment, changed type

* BTSE: added ordersizelimits seed data to test

* BTSE: fpair -> fPair naming & kline sort update

* BTSE: removed format call & asset param from withinLimits

* BTSE: range over pairs for active orders

* BTSE: verbose removal pass

* BTSE: ticker batching support

* BTSE: remove old pair formatter
This commit is contained in:
Andrew
2020-09-18 15:28:56 +10:00
committed by GitHub
parent 73ac8b90dc
commit 7592186a2a
8 changed files with 1495 additions and 431 deletions

View File

@@ -73,7 +73,7 @@ A helper tool [cmd/dbseed](../cmd/dbseed/README.md) has been created for assisti
| Bitstamp | Y |
| BTC Markets | Y |
| Bittrex | |
| BTSE | |
| BTSE | Y |
| Coinbase Pro | Y |
| Coinbene | Y |
| Coinut | |

View File

@@ -8,12 +8,16 @@ import (
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/common/crypto"
"github.com/thrasher-corp/gocryptotrader/currency"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"github.com/thrasher-corp/gocryptotrader/log"
)
@@ -25,99 +29,246 @@ type BTSE struct {
const (
btseAPIURL = "https://api.btse.com"
btseSPOTAPIPath = "/spot/v2/"
btseFuturesAPIPath = "/futures/api/v2.1/"
btseSPOTPath = "/spot"
btseSPOTAPIPath = "/api/v3.2/"
btseFuturesPath = "/futures"
btseFuturesAPIPath = "/api/v2.1/"
// Public endpoints
btseMarketOverview = "market_summary"
btseMarkets = "markets"
btseOrderbook = "orderbook"
btseTrades = "trades"
btseTicker = "ticker"
btseStats = "stats"
btseTime = "time"
btseOHLCV = "ohlcv"
btsePrice = "price"
// Authenticated endpoints
btseAccount = "account"
btseOrder = "order"
btsePendingOrders = "pending"
btseDeleteOrder = "deleteOrder"
btseFills = "fills"
btseTimeLayout = "2006-01-02 15:04:04"
btseWallet = "user/wallet"
btseWalletHistory = "user/wallet_history"
btseWalletAddress = "user/wallet/address"
btseWalletWithdrawal = "user/wallet/withdraw"
btseExchangeHistory = "user/trade_history"
btseUserFee = "user/fees"
btseOrder = "order"
btsePegOrder = "order/peg"
btsePendingOrders = "user/open_orders"
btseCancelAllAfter = "order/cancelAllAfter"
)
// GetMarketsSummary stores market summary data
func (b *BTSE) GetMarketsSummary() (*HighLevelMarketData, error) {
var m HighLevelMarketData
return &m, b.SendHTTPRequest(http.MethodGet, btseMarketOverview, &m, true)
}
// GetSpotMarkets returns a list of spot markets available on BTSE
func (b *BTSE) GetSpotMarkets() ([]SpotMarket, error) {
var m []SpotMarket
return m, b.SendHTTPRequest(http.MethodGet, btseMarkets, &m, true)
}
// GetFuturesMarkets returns a list of futures markets available on BTSE
func (b *BTSE) GetFuturesMarkets() ([]FuturesMarket, error) {
var m []FuturesMarket
return m, b.SendHTTPRequest(http.MethodGet, btseMarketOverview, &m, false)
// GetMarketSummary stores market summary data
func (b *BTSE) GetMarketSummary(symbol string, spot bool) (MarketSummary, error) {
var m MarketSummary
path := btseMarketOverview
if symbol != "" {
path += "?symbol=" + url.QueryEscape(symbol)
}
return m, b.SendHTTPRequest(http.MethodGet, path, &m, spot, queryFunc)
}
// FetchOrderBook gets orderbook data for a given pair
func (b *BTSE) FetchOrderBook(symbol string) (*Orderbook, error) {
func (b *BTSE) FetchOrderBook(symbol string, group, limitBids, limitAsks int, spot bool) (*Orderbook, error) {
var o Orderbook
endpoint := fmt.Sprintf("%s/%s", btseOrderbook, symbol)
return &o, b.SendHTTPRequest(http.MethodGet, endpoint, &o, true)
urlValues := url.Values{}
urlValues.Add("symbol", symbol)
if limitBids > 0 {
urlValues.Add("limit_bids", strconv.Itoa(limitBids))
}
if limitAsks > 0 {
urlValues.Add("limit_asks", strconv.Itoa(limitAsks))
}
if group > 0 {
urlValues.Add("group", strconv.Itoa(group))
}
return &o, b.SendHTTPRequest(http.MethodGet,
common.EncodeURLValues(btseOrderbook, urlValues), &o, spot, queryFunc)
}
// FetchOrderBookL2 retrieve level 2 orderbook for requested symbol and depth
func (b *BTSE) FetchOrderBookL2(symbol string, depth int) (*Orderbook, error) {
var o Orderbook
urlValues := url.Values{}
urlValues.Add("symbol", symbol)
urlValues.Add("depth", strconv.FormatInt(int64(depth), 10))
endpoint := common.EncodeURLValues(btseOrderbook+"/L2", urlValues)
return &o, b.SendHTTPRequest(http.MethodGet, endpoint, &o, true, queryFunc)
}
// GetTrades returns a list of trades for the specified symbol
func (b *BTSE) GetTrades(symbol string) ([]Trade, error) {
func (b *BTSE) GetTrades(symbol string, start, end time.Time, beforeSerialID, afterSerialID, count int, includeOld bool) ([]Trade, error) {
var t []Trade
endpoint := fmt.Sprintf("%s/%s", btseTrades, symbol)
return t, b.SendHTTPRequest(http.MethodGet, endpoint, &t, true)
}
// GetTicker returns the ticker for a specified symbol
func (b *BTSE) GetTicker(symbol string) (*Ticker, error) {
var t Ticker
endpoint := fmt.Sprintf("%s/%s", btseTicker, symbol)
err := b.SendHTTPRequest(http.MethodGet, endpoint, &t, true)
if err != nil {
return nil, err
urlValues := url.Values{}
urlValues.Add("symbol", symbol)
if count > 0 {
urlValues.Add("count", strconv.Itoa(count))
}
return &t, nil
if !start.IsZero() && !end.IsZero() {
if start.After(end) {
return t, errors.New("start cannot be after end time")
}
urlValues.Add("start", strconv.FormatInt(start.Unix(), 10))
urlValues.Add("end", strconv.FormatInt(end.Unix(), 10))
}
if beforeSerialID > 0 {
urlValues.Add("beforeSerialId", strconv.Itoa(beforeSerialID))
}
if afterSerialID > 0 {
urlValues.Add("afterSerialId", strconv.Itoa(afterSerialID))
}
if includeOld {
urlValues.Add("includeOld", "true")
}
return t, b.SendHTTPRequest(http.MethodGet,
common.EncodeURLValues(btseTrades, urlValues), &t, true, queryFunc)
}
// GetMarketStatistics gets market statistics for a specificed market
func (b *BTSE) GetMarketStatistics(symbol string) (*MarketStatistics, error) {
var m MarketStatistics
endpoint := fmt.Sprintf("%s/%s", btseStats, symbol)
return &m, b.SendHTTPRequest(http.MethodGet, endpoint, &m, true)
// OHLCV retrieve and return OHLCV candle data for requested symbol
func (b *BTSE) OHLCV(symbol string, start, end time.Time, resolution int) (OHLCV, error) {
var o OHLCV
urlValues := url.Values{}
urlValues.Add("symbol", symbol)
if !start.IsZero() && !end.IsZero() {
if start.After(end) {
return o, errors.New("start cannot be after end time")
}
urlValues.Add("start", strconv.FormatInt(start.Unix(), 10))
urlValues.Add("end", strconv.FormatInt(end.Unix(), 10))
}
var res = 60
if resolution != 0 {
res = resolution
}
urlValues.Add("resolution", strconv.FormatInt(int64(res), 10))
endpoint := common.EncodeURLValues(btseOHLCV, urlValues)
return o, b.SendHTTPRequest(http.MethodGet, endpoint, &o, true, queryFunc)
}
// GetPrice get current price for requested symbol
func (b *BTSE) GetPrice(symbol string) (Price, error) {
var p Price
path := btsePrice + "?symbol=" + url.QueryEscape(symbol)
return p, b.SendHTTPRequest(http.MethodGet, path, &p, true, queryFunc)
}
// GetServerTime returns the exchanges server time
func (b *BTSE) GetServerTime() (*ServerTime, error) {
var s ServerTime
return &s, b.SendHTTPRequest(http.MethodGet, btseTime, &s, true)
return &s, b.SendHTTPRequest(http.MethodGet, btseTime, &s, true, queryFunc)
}
// GetAccountBalance returns the users account balance
func (b *BTSE) GetAccountBalance() ([]CurrencyBalance, error) {
// GetWalletInformation returns the users account balance
func (b *BTSE) GetWalletInformation() ([]CurrencyBalance, error) {
var a []CurrencyBalance
return a, b.SendAuthenticatedHTTPRequest(http.MethodGet, btseAccount, nil, &a, true)
return a, b.SendAuthenticatedHTTPRequest(http.MethodGet, btseWallet, true, nil, nil, &a, queryFunc)
}
// GetFeeInformation retrieve fee's (maker/taker) for requested symbol
func (b *BTSE) GetFeeInformation(symbol string) ([]AccountFees, error) {
var resp []AccountFees
urlValues := url.Values{}
if symbol != "" {
urlValues.Add("symbol", symbol)
}
return resp, b.SendAuthenticatedHTTPRequest(http.MethodGet, btseUserFee, true, urlValues, nil, &resp, queryFunc)
}
// GetWalletHistory returns the users account balance
func (b *BTSE) GetWalletHistory(symbol string, start, end time.Time, count int) (WalletHistory, error) {
var resp WalletHistory
urlValues := url.Values{}
if symbol != "" {
urlValues.Add("symbol", symbol)
}
if !start.IsZero() && !end.IsZero() {
if start.After(end) || end.Before(start) {
return resp, errors.New("start cannot be after end time")
}
urlValues.Add("start", strconv.FormatInt(start.Unix(), 10))
urlValues.Add("end", strconv.FormatInt(end.Unix(), 10))
}
if count > 0 {
urlValues.Add("count", strconv.Itoa(count))
}
return resp, b.SendAuthenticatedHTTPRequest(http.MethodGet, btseWalletHistory, true, urlValues, nil, &resp, queryFunc)
}
// GetWalletAddress returns the users account balance
func (b *BTSE) GetWalletAddress(currency string) (WalletAddress, error) {
var resp WalletAddress
urlValues := url.Values{}
if currency != "" {
urlValues.Add("currency", currency)
}
return resp, b.SendAuthenticatedHTTPRequest(http.MethodGet, btseWalletAddress, true, urlValues, nil, &resp, queryFunc)
}
// CreateWalletAddress create new deposit address for requested currency
func (b *BTSE) CreateWalletAddress(currency string) (WalletAddress, error) {
var resp WalletAddress
req := make(map[string]interface{}, 1)
req["currency"] = currency
err := b.SendAuthenticatedHTTPRequest(http.MethodPost, btseWalletAddress, true, nil, req, &resp, queryFunc)
if err != nil {
errResp := ErrorResponse{}
errResponseStr := strings.Split(err.Error(), "raw response: ")
err := json.Unmarshal([]byte(errResponseStr[1]), &errResp)
if err != nil {
return resp, err
}
if errResp.ErrorCode == 3528 {
walletAddress := strings.Split(errResp.Message, "BADREQUEST: ")
return WalletAddress{
{
Address: walletAddress[1],
},
}, nil
}
return resp, err
}
return resp, nil
}
// WalletWithdrawal submit request to withdraw crypto currency
func (b *BTSE) WalletWithdrawal(currency, address, tag, amount string) (WithdrawalResponse, error) {
var resp WithdrawalResponse
req := make(map[string]interface{}, 4)
req["currency"] = currency
req["address"] = address
req["tag"] = tag
req["amount"] = amount
return resp, b.SendAuthenticatedHTTPRequest(http.MethodPost, btseWalletWithdrawal, true, nil, req, &resp, queryFunc)
}
// CreateOrder creates an order
func (b *BTSE) CreateOrder(amount, price float64, side, orderType, symbol, timeInForce, tag string) (*string, error) {
func (b *BTSE) CreateOrder(clOrderID string, deviation float64, postOnly bool, price float64, side string, size, stealth, stopPrice float64, symbol, timeInForce string, trailValue, triggerPrice float64, txType, orderType string) ([]Order, error) {
req := make(map[string]interface{})
req["amount"] = amount
req["price"] = price
if clOrderID != "" {
req["clOrderID"] = clOrderID
}
if deviation > 0.0 {
req["deviation"] = deviation
}
if postOnly {
req["postOnly"] = postOnly
}
if price > 0.0 {
req["price"] = price
}
if side != "" {
req["side"] = side
}
if orderType != "" {
req["type"] = orderType
if size > 0.0 {
req["size"] = size
}
if stealth > 0.0 {
req["stealth"] = stealth
}
if stopPrice > 0.0 {
req["stopPrice"] = stopPrice
}
if symbol != "" {
req["symbol"] = symbol
@@ -125,78 +276,149 @@ func (b *BTSE) CreateOrder(amount, price float64, side, orderType, symbol, timeI
if timeInForce != "" {
req["time_in_force"] = timeInForce
}
if tag != "" {
req["tag"] = tag
if trailValue > 0.0 {
req["trailValue"] = trailValue
}
if triggerPrice > 0.0 {
req["triggerPrice"] = triggerPrice
}
if txType != "" {
req["txType"] = txType
}
if orderType != "" {
req["type"] = orderType
}
type orderResp struct {
ID string `json:"id"`
}
var r orderResp
return &r.ID, b.SendAuthenticatedHTTPRequest(http.MethodPost, btseOrder, req, &r, true)
var r []Order
return r, b.SendAuthenticatedHTTPRequest(http.MethodPost, btseOrder, true, url.Values{}, req, &r, orderFunc)
}
// GetOrders returns all pending orders
func (b *BTSE) GetOrders(symbol string) ([]OpenOrder, error) {
req := make(map[string]interface{})
if symbol != "" {
req["symbol"] = symbol
func (b *BTSE) GetOrders(symbol, orderID, clOrderID string) ([]OpenOrder, error) {
req := url.Values{}
if orderID != "" {
req.Add("orderID", orderID)
}
req.Add("symbol", symbol)
if clOrderID != "" {
req.Add("clOrderID", clOrderID)
}
var o []OpenOrder
return o, b.SendAuthenticatedHTTPRequest(http.MethodGet, btsePendingOrders, req, &o, true)
return o, b.SendAuthenticatedHTTPRequest(http.MethodGet, btsePendingOrders, true, req, nil, &o, orderFunc)
}
// CancelExistingOrder cancels an order
func (b *BTSE) CancelExistingOrder(orderID, symbol string) (*CancelOrder, error) {
func (b *BTSE) CancelExistingOrder(orderID, symbol, clOrderID string) (CancelOrder, error) {
var c CancelOrder
req := make(map[string]interface{})
req["order_id"] = orderID
req["symbol"] = symbol
return &c, b.SendAuthenticatedHTTPRequest(http.MethodPost, btseDeleteOrder, req, &c, true)
req := url.Values{}
if orderID != "" {
req.Add("orderID", orderID)
}
req.Add("symbol", symbol)
if clOrderID != "" {
req.Add("clOrderID", clOrderID)
}
return c, b.SendAuthenticatedHTTPRequest(http.MethodDelete, btseOrder, true, req, nil, &c, orderFunc)
}
// GetFills gets all filled orders
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")
}
// CancelAllAfter cancels all orders after timeout
func (b *BTSE) CancelAllAfter(timeout int) error {
req := make(map[string]interface{})
if orderID != "" {
req["order_id"] = orderID
}
req["timeout"] = timeout
return b.SendAuthenticatedHTTPRequest(http.MethodPost, btseCancelAllAfter, true, url.Values{}, req, nil, orderFunc)
}
// IndexOrderPeg create peg order that will track a certain percentage above/below the index price
func (b *BTSE) IndexOrderPeg(clOrderID string, deviation float64, postOnly bool, price float64, side string, size, stealth, stopPrice float64, symbol, timeInForce string, trailValue, triggerPrice float64, txType, orderType string) ([]Order, error) {
var o []Order
req := make(map[string]interface{})
if clOrderID != "" {
req["clOrderID"] = clOrderID
}
if deviation > 0.0 {
req["deviation"] = deviation
}
if postOnly {
req["postOnly"] = postOnly
}
if price > 0.0 {
req["price"] = price
}
if side != "" {
req["side"] = side
}
if size > 0.0 {
req["size"] = size
}
if stealth > 0.0 {
req["stealth"] = stealth
}
if stopPrice > 0.0 {
req["stopPrice"] = stopPrice
}
if symbol != "" {
req["symbol"] = symbol
}
if before != "" {
req["before"] = before
if timeInForce != "" {
req["time_in_force"] = timeInForce
}
if trailValue > 0.0 {
req["trailValue"] = trailValue
}
if triggerPrice > 0.0 {
req["triggerPrice"] = triggerPrice
}
if txType != "" {
req["txType"] = txType
}
if orderType != "" {
req["type"] = orderType
}
if after != "" {
req["after"] = after
}
return o, b.SendAuthenticatedHTTPRequest(http.MethodPost, btsePegOrder, true, url.Values{}, req, nil, orderFunc)
}
if limit != "" {
req["limit"] = limit
// TradeHistory returns previous trades on exchange
func (b *BTSE) TradeHistory(symbol string, start, end time.Time, beforeSerialID, afterSerialID, count int, includeOld bool, clOrderID, orderID string) (TradeHistory, error) {
var resp TradeHistory
urlValues := url.Values{}
if symbol != "" {
urlValues.Add("symbol", symbol)
}
if username != "" {
req["username"] = username
if !start.IsZero() && !end.IsZero() {
if start.After(end) || end.Before(start) {
return resp, errors.New("start and end must both be valid")
}
urlValues.Add("start", strconv.FormatInt(start.Unix(), 10))
urlValues.Add("end", strconv.FormatInt(end.Unix(), 10))
}
var o []FilledOrder
return o, b.SendAuthenticatedHTTPRequest(http.MethodPost, btseFills, req, &o, true)
if beforeSerialID > 0 {
urlValues.Add("beforeSerialId", strconv.Itoa(beforeSerialID))
}
if afterSerialID > 0 {
urlValues.Add("afterSerialId", strconv.Itoa(afterSerialID))
}
if includeOld {
urlValues.Add("includeOld", "true")
}
if count > 0 {
urlValues.Add("count", strconv.Itoa(count))
}
if clOrderID != "" {
urlValues.Add("clOrderId", clOrderID)
}
if orderID != "" {
urlValues.Add("orderID", orderID)
}
return resp, b.SendAuthenticatedHTTPRequest(http.MethodGet, btseExchangeHistory, true, urlValues, nil, &resp, queryFunc)
}
// SendHTTPRequest sends an HTTP request to the desired endpoint
func (b *BTSE) SendHTTPRequest(method, endpoint string, result interface{}, spotEndpoint bool) error {
p := btseSPOTAPIPath
func (b *BTSE) SendHTTPRequest(method, endpoint string, result interface{}, spotEndpoint bool, f request.EndpointLimit) error {
p := btseSPOTPath + btseSPOTAPIPath
if !spotEndpoint {
p = btseFuturesAPIPath
p = btseFuturesPath + btseFuturesAPIPath
}
return b.SendPayload(context.Background(), &request.Item{
Method: method,
@@ -205,66 +427,76 @@ func (b *BTSE) SendHTTPRequest(method, endpoint string, result interface{}, spot
Verbose: b.Verbose,
HTTPDebugging: b.HTTPDebugging,
HTTPRecording: b.HTTPRecording,
Endpoint: f,
})
}
// SendAuthenticatedHTTPRequest sends an authenticated HTTP request to the desired endpoint
func (b *BTSE) SendAuthenticatedHTTPRequest(method, endpoint string, req map[string]interface{}, result interface{}, spotEndpoint bool) error {
func (b *BTSE) SendAuthenticatedHTTPRequest(method, endpoint string, isSpot bool, values url.Values, req map[string]interface{}, result interface{}, f request.EndpointLimit) error {
if !b.AllowAuthenticatedRequest() {
return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet,
b.Name)
}
p := btseSPOTAPIPath
if !spotEndpoint {
p = btseFuturesAPIPath
// The concatenation is done this way because BTSE expect endpoint+nonce or endpoint+nonce+body
// when signing the data but the full path of the request is /spot/api/v3.2/<endpoint>
// its messy but it works and supports futures as well
host := b.API.Endpoints.URL
if isSpot {
host += btseSPOTPath + btseSPOTAPIPath + endpoint
endpoint = btseSPOTAPIPath + endpoint
} else {
host += btseFuturesPath + btseFuturesAPIPath
endpoint += btseFuturesAPIPath
}
path := p + endpoint
headers := make(map[string]string)
headers["btse-api"] = b.API.Credentials.Key
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 = json.Marshal(req)
var body io.Reader
nonce := strconv.FormatInt(time.Now().UnixNano()/int64(time.Millisecond), 10)
headers := map[string]string{
"btse-api": b.API.Credentials.Key,
"btse-nonce": nonce,
}
if req != nil {
reqPayload, err := json.Marshal(req)
if err != nil {
return err
}
body = bytes.NewBuffer(payload)
body = bytes.NewBuffer(reqPayload)
hmac = crypto.GetHMAC(
crypto.HashSHA512_384,
[]byte((path + nonce + string(payload))),
[]byte((endpoint + nonce + string(reqPayload))),
[]byte(b.API.Credentials.Secret),
)
headers["Content-Type"] = "application/json"
} else {
hmac = crypto.GetHMAC(
crypto.HashSHA512_384,
[]byte((path + nonce)),
[]byte((endpoint + nonce)),
[]byte(b.API.Credentials.Secret),
)
if len(values) > 0 {
host += "?" + values.Encode()
}
}
headers["btse-sign"] = crypto.HexEncodeToString(hmac)
if b.Verbose {
log.Debugf(log.ExchangeSys,
"%s Sending %s request to URL %s with params %s\n",
b.Name, method, path, string(payload))
"%s Sending %s request to URL %s",
b.Name, method, endpoint)
}
return b.SendPayload(context.Background(), &request.Item{
Method: method,
Path: b.API.Endpoints.URL + path,
Path: host,
Headers: headers,
Body: body,
Result: result,
AuthRequest: true,
NonceEnabled: true,
Verbose: b.Verbose,
HTTPDebugging: b.HTTPDebugging,
HTTPRecording: b.HTTPRecording,
Endpoint: f,
})
}
@@ -274,7 +506,7 @@ func (b *BTSE) GetFee(feeBuilder *exchange.FeeBuilder) (float64, error) {
switch feeBuilder.FeeType {
case exchange.CryptocurrencyTradeFee:
fee = calculateTradingFee(feeBuilder.IsMaker) * feeBuilder.Amount * feeBuilder.PurchasePrice
fee = b.calculateTradingFee(feeBuilder) * feeBuilder.Amount * feeBuilder.PurchasePrice
case exchange.CryptocurrencyWithdrawalFee:
switch feeBuilder.Pair.Base {
case currency.USDT:
@@ -329,16 +561,28 @@ func getInternationalBankWithdrawalFee(amount float64) float64 {
return fee
}
// calculateTradingFee BTSE has fee tiers, but does not disclose them via API,
// so the largest fee has to be assumed
func calculateTradingFee(isMaker bool) float64 {
fee := 0.00050
if !isMaker {
fee = 0.001
// calculateTradingFee return fee based on users current fee tier or default values
func (b *BTSE) calculateTradingFee(feeBuilder *exchange.FeeBuilder) float64 {
formattedPair, err := b.FormatExchangeCurrency(feeBuilder.Pair, asset.Spot)
if err != nil {
if feeBuilder.IsMaker {
return 0.001
}
return 0.002
}
return fee
feeTiers, err := b.GetFeeInformation(formattedPair.String())
if err != nil {
if feeBuilder.IsMaker {
return 0.001
}
return 0.002
}
if feeBuilder.IsMaker {
return feeTiers[0].MakerFee
}
return feeTiers[0].TakerFee
}
func parseOrderTime(timeStr string) (time.Time, error) {
return time.Parse(btseTimeLayout, timeStr)
return time.Parse(common.SimpleTimeFormat, timeStr)
}

View File

@@ -1,15 +1,20 @@
package btse
import (
"errors"
"log"
"os"
"strings"
"testing"
"time"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/config"
"github.com/thrasher-corp/gocryptotrader/core"
"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/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/sharedtestvalues"
)
@@ -53,57 +58,186 @@ func areTestAPIKeysSet() bool {
func TestGetMarketsSummary(t *testing.T) {
t.Parallel()
_, err := b.GetMarketsSummary()
_, err := b.GetMarketSummary("", true)
if err != nil {
t.Error(err)
}
}
func TestGetSpotMarkets(t *testing.T) {
t.Parallel()
_, err := b.GetSpotMarkets()
ret, err := b.GetMarketSummary(testPair, true)
if err != nil {
t.Error(err)
}
}
func TestGetFuturesMarkets(t *testing.T) {
t.Parallel()
_, err := b.GetFuturesMarkets()
if err != nil {
t.Error(err)
if len(ret) != 1 {
t.Errorf("expected only one result when requesting BTC-USD data received: %v", len(ret))
}
}
func TestFetchOrderBook(t *testing.T) {
t.Parallel()
_, err := b.FetchOrderBook(testPair)
_, err := b.FetchOrderBook(testPair, 0, 1, 1, true)
if err != nil {
t.Error(err)
}
_, err = b.FetchOrderBook("BTCPFC", 0, 1, 1, false)
if err != nil {
t.Error(err)
}
_, err = b.FetchOrderBook(testPair, 1, 1, 1, true)
if err != nil {
t.Error(err)
}
}
func TestUpdateOrderbook(t *testing.T) {
t.Parallel()
p, err := currency.NewPairFromString(testPair)
if err != nil {
t.Fatal(err)
}
_, err = b.UpdateOrderbook(p, asset.Spot)
if err != nil {
t.Fatal(err)
}
f, err := currency.NewPairFromString("BTC-PFC")
if err != nil {
t.Fatal(err)
}
_, err = b.UpdateOrderbook(f, asset.Futures)
if err != nil {
if !errors.Is(err, common.ErrNotYetImplemented) {
t.Fatal(err)
}
}
}
func TestFetchOrderBookL2(t *testing.T) {
t.Parallel()
_, err := b.FetchOrderBookL2(testPair, 20)
if err != nil {
t.Error(err)
}
}
func TestOHLCV(t *testing.T) {
t.Parallel()
_, err := b.OHLCV(testPair,
time.Now().AddDate(0, 0, -1),
time.Now(), 60)
if err != nil {
t.Fatal(err)
}
_, err = b.OHLCV(testPair, time.Now(), time.Now().AddDate(0, 0, -1), 60)
if err == nil {
t.Fatal("expected error if start is after end date")
}
}
func TestGetPrice(t *testing.T) {
t.Parallel()
_, err := b.GetPrice(testPair)
if err != nil {
t.Fatal(err)
}
}
func TestFormatExchangeKlineInterval(t *testing.T) {
ret := b.FormatExchangeKlineInterval(kline.OneDay)
if ret != "1440" {
t.Fatalf("unexpected result received: %v", ret)
}
}
func TestGetHistoricCandles(t *testing.T) {
t.Parallel()
curr, err := currency.NewPairFromString(testPair)
if err != nil {
t.Fatal(err)
}
_, err = b.GetHistoricCandles(
curr, asset.Spot,
time.Time{}, time.Time{},
kline.OneMin)
if err != nil {
t.Fatal(err)
}
_, err = b.GetHistoricCandles(
curr, asset.Spot,
time.Time{}, time.Time{},
kline.OneDay)
if err != nil {
t.Fatal(err)
}
curr.Quote = currency.XRP
_, err = b.GetHistoricCandles(
curr, asset.Spot,
time.Time{}, time.Time{},
kline.OneMin)
if err == nil {
t.Fatal("expected error when requesting with disabled pair")
}
}
func TestGetHistoricCandlesExtended(t *testing.T) {
t.Parallel()
curr, err := currency.NewPairFromString(testPair)
if err != nil {
t.Fatal(err)
}
_, err = b.GetHistoricCandlesExtended(
curr, asset.Spot,
time.Time{}, time.Time{},
kline.OneMin)
if err != nil {
t.Fatal(err)
}
}
func TestGetTrades(t *testing.T) {
t.Parallel()
_, err := b.GetTrades(testPair)
_, err := b.GetTrades(testPair,
time.Now().AddDate(0, 0, -1), time.Now(),
0, 0, 50, false)
if err != nil {
t.Error(err)
}
}
func TestGetTicker(t *testing.T) {
t.Parallel()
_, err := b.GetTicker(testPair)
if err != nil {
t.Error(err)
_, err = b.GetTrades(testPair,
time.Now(), time.Now().AddDate(0, -1, 0),
0, 0, 50, false)
if err == nil {
t.Error("expected error if start time is after end time")
}
}
func TestGetMarketStatistics(t *testing.T) {
func TestUpdateTicker(t *testing.T) {
t.Parallel()
_, err := b.GetMarketStatistics(testPair)
curr, err := currency.NewPairFromString(testPair)
if err != nil {
t.Error(err)
t.Fatal(err)
}
_, err = b.UpdateTicker(curr, asset.Spot)
if err != nil {
t.Fatal(err)
}
curr, err = currency.NewPairFromString("BTC-PFC")
if err != nil {
t.Fatal(err)
}
_, err = b.UpdateTicker(curr, asset.Futures)
if err != nil {
t.Fatal(err)
}
}
@@ -115,23 +249,69 @@ func TestGetServerTime(t *testing.T) {
}
}
func TestGetAccount(t *testing.T) {
func TestGetWalletInformation(t *testing.T) {
t.Parallel()
if !areTestAPIKeysSet() {
t.Skip("API keys not set, skipping test")
}
_, err := b.GetAccountBalance()
_, err := b.GetWalletInformation()
if err != nil {
t.Error(err)
}
}
func TestGetFills(t *testing.T) {
func TestGetFeeInformation(t *testing.T) {
t.Parallel()
if !areTestAPIKeysSet() {
t.Skip("API keys not set, skipping test")
}
_, err := b.GetFills("", testPair, "", "", "", "")
_, err := b.GetFeeInformation("")
if err != nil {
t.Error(err)
}
}
func TestGetWalletHistory(t *testing.T) {
t.Parallel()
if !areTestAPIKeysSet() {
t.Skip("API keys not set, skipping test")
}
_, err := b.GetWalletHistory(testPair,
time.Time{}, time.Time{},
50)
if err != nil {
t.Error(err)
}
}
func TestGetWalletAddress(t *testing.T) {
t.Parallel()
if !areTestAPIKeysSet() {
t.Skip("API keys not set, skipping test")
}
_, err := b.GetWalletAddress("XRP")
if err != nil {
t.Error(err)
}
}
func TestCreateWalletAddress(t *testing.T) {
t.Parallel()
if !areTestAPIKeysSet() {
t.Skip("API keys not set, skipping test")
}
_, err := b.CreateWalletAddress("XRP")
if err != nil {
t.Error(err)
}
}
func TestGetDepositAddress(t *testing.T) {
t.Parallel()
if !areTestAPIKeysSet() {
t.Skip("API keys not set, skipping test")
}
_, err := b.GetDepositAddress(currency.XRP, "")
if err != nil {
t.Error(err)
}
@@ -140,15 +320,30 @@ func TestGetFills(t *testing.T) {
func TestCreateOrder(t *testing.T) {
t.Parallel()
if !areTestAPIKeysSet() || !canManipulateRealOrders {
t.Skip("skipping test, either api keys or manipulaterealorders isnt set correctly")
t.Skip("skipping test, either api keys are unset or canManipulateRealOrders is false")
}
_, err := b.CreateOrder(0.1,
10000,
order.Sell.String(),
order.Limit.String(),
testPair,
"",
"")
_, err := b.CreateOrder("", 0.0,
false,
-1, "BUY", 100, 0, 0,
testPair, "GTC",
0.0, 0.0,
"LIMIT", "LIMIT")
if err != nil {
t.Error(err)
}
}
func TestBTSEIndexOrderPeg(t *testing.T) {
t.Parallel()
if !areTestAPIKeysSet() || !canManipulateRealOrders {
t.Skip("skipping test, either api keys are unset or canManipulateRealOrders is false")
}
_, err := b.IndexOrderPeg("", 0.0,
false,
-1, "BUY", 100, 0, 0,
testPair, "GTC",
0.0, 0.0,
"", "LIMIT")
if err != nil {
t.Error(err)
}
@@ -159,7 +354,7 @@ func TestGetOrders(t *testing.T) {
if !areTestAPIKeysSet() {
t.Skip("API keys not set, skipping test")
}
_, err := b.GetOrders("")
_, err := b.GetOrders(testPair, "", "")
if err != nil {
t.Error(err)
}
@@ -171,6 +366,18 @@ func TestGetActiveOrders(t *testing.T) {
t.Skip("API keys not set, skipping test")
}
var getOrdersRequest = order.GetOrdersRequest{
Pairs: []currency.Pair{
{
Delimiter: "-",
Base: currency.BTC,
Quote: currency.USD,
},
{
Delimiter: "-",
Base: currency.XRP,
Quote: currency.USD,
},
},
Type: order.AnyType,
}
@@ -180,6 +387,21 @@ func TestGetActiveOrders(t *testing.T) {
}
}
func TestGetExchangeHistory(t *testing.T) {
curr, _ := currency.NewPairFromString(testPair)
_, err := b.GetExchangeHistory(curr, asset.Spot, time.Now().AddDate(0, -6, 0), time.Now())
if err != nil {
t.Fatal(err)
}
_, err = b.GetExchangeHistory(curr, asset.Futures, time.Now().AddDate(0, -6, 0), time.Now())
if err != nil {
if !errors.Is(err, common.ErrNotYetImplemented) {
t.Fatal(err)
}
}
}
func TestGetOrderHistory(t *testing.T) {
t.Parallel()
if !areTestAPIKeysSet() {
@@ -194,6 +416,21 @@ func TestGetOrderHistory(t *testing.T) {
}
}
func TestTradeHistory(t *testing.T) {
t.Parallel()
if !areTestAPIKeysSet() {
t.Skip("API keys not set, skipping test")
}
_, err := b.TradeHistory("",
time.Time{}, time.Time{},
0, 0, 0,
false,
"", "")
if err != nil {
t.Fatal(err)
}
}
func TestFormatWithdrawPermissions(t *testing.T) {
t.Parallel()
expected := exchange.NoAPIWithdrawalMethodsText
@@ -236,14 +473,14 @@ func TestGetFee(t *testing.T) {
PurchasePrice: 1000,
}
if resp, err := b.GetFee(feeBuilder); resp != 0.500000 || err != nil {
t.Errorf("GetFee() error. Expected: %f, Received: %f", 0.500000, resp)
if resp, err := b.GetFee(feeBuilder); resp != 1.000000 || err != nil {
t.Errorf("GetFee() error. Expected: %f, Received: %f", 1.000000, resp)
t.Error(err)
}
feeBuilder.IsMaker = false
if resp, err := b.GetFee(feeBuilder); resp != 1.00000 || err != nil {
t.Errorf("GetFee() error. Expected: %f, Received: %f", 1.00000, resp)
if resp, err := b.GetFee(feeBuilder); resp != 2.00000 || err != nil {
t.Errorf("GetFee() error. Expected: %f, Received: %f", 2.00000, resp)
t.Error(err)
}
@@ -285,7 +522,7 @@ func TestGetFee(t *testing.T) {
}
func TestParseOrderTime(t *testing.T) {
expected := int64(1534794360)
expected := int64(1534792846)
actual, err := parseOrderTime("2018-08-20 19:20:46")
if err != nil {
t.Fatal(err)
@@ -300,18 +537,19 @@ func TestParseOrderTime(t *testing.T) {
func TestSubmitOrder(t *testing.T) {
t.Parallel()
if !areTestAPIKeysSet() || !canManipulateRealOrders {
t.Skip("skipping test, either api keys or manipulaterealorders isnt set correctly")
t.Skip("skipping test, either api keys are unset or canManipulateRealOrders is false")
}
var orderSubmission = &order.Submit{
Pair: currency.Pair{
Base: currency.BTC,
Quote: currency.USD,
},
Side: order.Buy,
Type: order.Limit,
Price: 100000,
Amount: 0.1,
ClientID: "meowOrder",
Side: order.Buy,
Type: order.Limit,
Price: -100000000,
Amount: 1,
ClientID: "",
AssetType: asset.Spot,
}
response, err := b.SubmitOrder(orderSubmission)
if areTestAPIKeysSet() && (err != nil || !response.IsOrderPlaced) {
@@ -321,10 +559,22 @@ func TestSubmitOrder(t *testing.T) {
}
}
func TestCancelAllAfter(t *testing.T) {
t.Parallel()
if !areTestAPIKeysSet() || !canManipulateRealOrders {
t.Skip("skipping test, either api keys are unset or canManipulateRealOrders is false")
}
err := b.CancelAllAfter(1)
if err != nil {
t.Fatal(err)
}
}
func TestCancelExchangeOrder(t *testing.T) {
t.Parallel()
if !areTestAPIKeysSet() || !canManipulateRealOrders {
t.Skip("skipping test, either api keys or manipulaterealorders isnt set correctly")
t.Skip("skipping test, either api keys are unset or canManipulateRealOrders is false")
}
currencyPair := currency.NewPairWithDelimiter(currency.BTC.String(),
currency.USD.String(),
@@ -334,6 +584,7 @@ func TestCancelExchangeOrder(t *testing.T) {
WalletAddress: core.BitcoinDonationAddress,
AccountID: "1",
Pair: currencyPair,
AssetType: asset.Spot,
}
err := b.CancelOrder(orderCancellation)
if err != nil {
@@ -341,10 +592,21 @@ func TestCancelExchangeOrder(t *testing.T) {
}
}
func TestCancelOrder(t *testing.T) {
t.Parallel()
if !areTestAPIKeysSet() || !canManipulateRealOrders {
t.Skip("skipping test, either api keys are unset or canManipulateRealOrders is false")
}
_, err := b.CancelExistingOrder("", testPair, "")
if err != nil {
t.Fatal(err)
}
}
func TestCancelAllExchangeOrders(t *testing.T) {
t.Parallel()
if !areTestAPIKeysSet() || !canManipulateRealOrders {
t.Skip("skipping test, either api keys or manipulaterealorders isnt set correctly")
t.Skip("skipping test, either api keys are unset or canManipulateRealOrders is false")
}
currencyPair := currency.NewPairWithDelimiter(currency.BTC.String(),
currency.USD.String(),
@@ -354,6 +616,7 @@ func TestCancelAllExchangeOrders(t *testing.T) {
WalletAddress: core.BitcoinDonationAddress,
AccountID: "1",
Pair: currencyPair,
AssetType: asset.Spot,
}
resp, err := b.CancelAllOrders(orderCancellation)
@@ -419,6 +682,7 @@ func TestStatusToStandardStatus(t *testing.T) {
}
func TestFetchTradablePairs(t *testing.T) {
t.Parallel()
assets := b.GetAssetTypes()
for i := range assets {
data, err := b.FetchTradablePairs(assets[i])
@@ -430,3 +694,132 @@ func TestFetchTradablePairs(t *testing.T) {
}
}
}
func TestMatchType(t *testing.T) {
t.Parallel()
ret := matchType(1, order.AnyType)
if !ret {
t.Fatal("expected true value")
}
ret = matchType(76, order.Market)
if ret {
t.Fatal("expected false match")
}
ret = matchType(76, order.Limit)
if !ret {
t.Fatal("expected true match")
}
ret = matchType(77, order.Market)
if !ret {
t.Fatal("expected true match")
}
}
func TestSeedOrderSizeLimits(t *testing.T) {
t.Parallel()
err := b.seedOrderSizeLimits()
if err != nil {
t.Fatal(err)
}
}
func TestOrderSizeLimits(t *testing.T) {
t.Parallel()
seedOrderSizeLimitMap()
_, ok := OrderSizeLimits(testPair)
if !ok {
t.Fatal("expected BTC-USD to be found in map")
}
_, ok = OrderSizeLimits("XRP-GARBAGE")
if ok {
t.Fatal("expected false value for XRP-GARBAGE")
}
}
func seedOrderSizeLimitMap() {
testOrderSizeLimits := []struct {
name string
o OrderSizeLimit
}{
{
name: "XRP-USD",
o: OrderSizeLimit{
MinSizeIncrement: 1,
MinOrderSize: 1,
MaxOrderSize: 1000000,
},
},
{
name: "LTC-USD",
o: OrderSizeLimit{
MinSizeIncrement: 0.01,
MinOrderSize: 0.01,
MaxOrderSize: 5000,
},
},
{
name: "BTC-USD",
o: OrderSizeLimit{
MinSizeIncrement: 0.0001,
MinOrderSize: 1,
MaxOrderSize: 1000000,
},
},
}
orderSizeLimitMap.Range(func(key interface{}, _ interface{}) bool {
orderSizeLimitMap.Delete(key)
return true
})
for x := range testOrderSizeLimits {
orderSizeLimitMap.Store(testOrderSizeLimits[x].name, testOrderSizeLimits[x].o)
}
}
func TestWithinLimits(t *testing.T) {
t.Parallel()
seedOrderSizeLimitMap()
p, _ := currency.NewPairDelimiter("XRP-USD", "-")
v := b.withinLimits(p, 1.0)
if !v {
t.Fatal("expected valid limits")
}
v = b.withinLimits(p, 5.0000001)
if v {
t.Fatal("expected invalid limits")
}
v = b.withinLimits(p, 100)
if !v {
t.Fatal("expected valid limits")
}
v = b.withinLimits(p, 10.1)
if v {
t.Fatal("expected invalid limits")
}
p.Base = currency.LTC
v = b.withinLimits(p, 10)
if v {
t.Fatal("expected valid limits")
}
v = b.withinLimits(p, 0.009)
if !v {
t.Fatal("expected invalid limits")
}
p.Base = currency.BTC
v = b.withinLimits(p, 10)
if v {
t.Fatal("expected valid limits")
}
v = b.withinLimits(p, 0.001)
if !v {
t.Fatal("expected invalid limits")
}
}

View File

@@ -1,25 +1,65 @@
package btse
import "time"
import (
"sync"
"time"
)
const (
// Default order type is good till cancel (or filled)
goodTillCancel = "gtc"
goodTillCancel = "GTC"
orderInserted = 2
orderCancelled = 6
)
// 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"`
// MarketSummary response data
type MarketSummary []struct {
Symbol string `json:"symbol"`
Last float64 `json:"last"`
LowestAsk float64 `json:"lowestAsk"`
HighestBid float64 `json:"highestBid"`
PercentageChange float64 `json:"percentageChange"`
Volume float64 `json:"volume"`
High24Hr float64 `json:"high24Hr"`
Low24Hr float64 `json:"low24Hr"`
Base string `json:"base"`
Quote string `json:"quote"`
Active bool `json:"active"`
Size float64 `json:"size"`
MinValidPrice float64 `json:"minValidPrice"`
MinPriceIncrement float64 `json:"minPriceIncrement"`
MinOrderSize float64 `json:"minOrderSize"`
MaxOrderSize float64 `json:"maxOrderSize"`
MinSizeIncrement float64 `json:"minSizeIncrement"`
OpenInterest float64 `json:"openInterest"`
OpenInterestUSD float64 `json:"openInterestUSD"`
ContractStart int64 `json:"contractStart"`
ContractEnd int64 `json:"contractEnd"`
TimeBasedContract bool `json:"timeBasedContract"`
OpenTime int64 `json:"openTime"`
CloseTime int64 `json:"closeTime"`
StartMatching int64 `json:"startMatching"`
InactiveTime int64 `json:"inactiveTime"`
FundingRate float64 `json:"fundingRate"`
ContractSize float64 `json:"contractSize"`
MaxPosition int64 `json:"maxPosition"`
MinRiskLimit int `json:"minRiskLimit"`
MaxRiskLimit int `json:"maxRiskLimit"`
AvailableSettlement []string `json:"availableSettlement"`
Futures bool `json:"futures"`
}
// HighLevelMarketData stores market overview data
type HighLevelMarketData map[string]OverviewData
// OHLCV holds Open, High Low, Close, Volume data for set symbol
type OHLCV [][]float64
// Price stores last price for requested symbol
type Price []struct {
IndexPrice float64 `json:"indexPrice"`
LastPrice float64 `json:"lastPrice"`
MarkPrice float64 `json:"markPrice"`
Symbol string `json:"symbol"`
}
// SpotMarket stores market data
type SpotMarket struct {
@@ -72,11 +112,12 @@ type FuturesMarket struct {
// Trade stores trade data
type Trade struct {
SerialID string `json:"serial_id"`
SerialID int `json:"serialId"`
Symbol string `json:"symbol"`
Price float64 `json:"price"`
Amount float64 `json:"amount"`
Time string `json:"time"`
Amount float64 `json:"size"`
Time int64 `json:"timestamp"`
Side string `json:"side"`
Type string `json:"type"`
}
@@ -117,52 +158,125 @@ type MarketStatistics struct {
// ServerTime stores the server time data
type ServerTime struct {
ISO time.Time `json:"iso"`
Epoch string `json:"epoch"`
Epoch int64 `json:"epoch"`
}
// CurrencyBalance stores the account info data
type CurrencyBalance struct {
Currency string `json:"currency"`
Total float64 `json:"total,string"`
Available float64 `json:"available,string"`
Total float64 `json:"total"`
Available float64 `json:"available"`
}
// Order stores the order info
type Order struct {
ID string `json:"id"`
Type string `json:"type"`
Side string `json:"side"`
Price float64 `json:"price"`
Amount float64 `json:"amount"`
Tag string `json:"tag"`
Symbol string `json:"symbol"`
CreatedAt string `json:"created_at"`
// AccountFees stores fee for each currency pair
type AccountFees struct {
MakerFee float64 `json:"makerFee"`
Symbol string `json:"symbol"`
TakerFee float64 `json:"takerFee"`
}
// TradeHistory stores user trades for exchange
type TradeHistory []struct {
Base string `json:"base"`
ClOrderID string `json:"clOrderID"`
FeeAmount float64 `json:"feeAmount"`
FeeCurrency string `json:"feeCurrency"`
FilledPrice float64 `json:"filledPrice"`
FilledSize float64 `json:"filledSize"`
OrderID string `json:"orderId"`
OrderType int `json:"orderType"`
Price float64 `json:"price"`
Quote string `json:"quote"`
RealizedPnl float64 `json:"realizedPnl"`
SerialID int64 `json:"serialId"`
Side string `json:"side"`
Size float64 `json:"size"`
Symbol string `json:"symbol"`
Timestamp string `json:"timestamp"`
Total float64 `json:"total"`
TradeID string `json:"tradeId"`
TriggerPrice float64 `json:"triggerPrice"`
TriggerType int `json:"triggerType"`
Username string `json:"username"`
Wallet string `json:"wallet"`
}
// WalletHistory stores account funding history
type WalletHistory []struct {
Amount float64 `json:"amount"`
Currency string `json:"currency"`
Description string `json:"description"`
Fees float64 `json:"fees"`
OrderID string `json:"orderId"`
Status string `json:"status"`
Timestamp int64 `json:"timestamp"`
Type string `json:"type"`
Username string `json:"username"`
Wallet string `json:"wallet"`
}
// WalletAddress stores address for crypto deposit's
type WalletAddress []struct {
Address string `json:"address"`
Created int `json:"created"`
}
// WithdrawalResponse response received when submitting a crypto withdrawal request
type WithdrawalResponse struct {
WithdrawID string `json:"withdraw_id"`
}
// OpenOrder stores an open order info
type OpenOrder struct {
Order
Status string `json:"status"`
AverageFillPrice float64 `json:"averageFillPrice"`
CancelDuration int64 `json:"cancelDuration"`
ClOrderID string `json:"clOrderID"`
FillSize float64 `json:"fillSize"`
FilledSize float64 `json:"filledSize"`
OrderID string `json:"orderID"`
OrderState string `json:"orderState"`
OrderType int `json:"orderType"`
OrderValue float64 `json:"orderValue"`
PegPriceDeviation float64 `json:"pegPriceDeviation"`
PegPriceMax float64 `json:"pegPriceMax"`
PegPriceMin float64 `json:"pegPriceMin"`
Price float64 `json:"price"`
Side string `json:"side"`
Size float64 `json:"size"`
Symbol string `json:"symbol"`
Timestamp int64 `json:"timestamp"`
TrailValue float64 `json:"trailValue"`
TriggerOrder bool `json:"triggerOrder"`
TriggerOrderType int `json:"triggerOrderType"`
TriggerOriginalPrice float64 `json:"triggerOriginalPrice"`
TriggerPrice float64 `json:"triggerPrice"`
TriggerStopPrice float64 `json:"triggerStopPrice"`
TriggerTrailingStopDeviation float64 `json:"triggerTrailingStopDeviation"`
Triggered bool `json:"triggered"`
}
// CancelOrder stores the cancel order response data
type CancelOrder struct {
Code int `json:"code"`
Time int64 `json:"time"`
}
// CancelOrder stores slice of orders
type CancelOrder []Order
// FilledOrder stores filled order data
type FilledOrder struct {
Price float64 `json:"price"`
Amount float64 `json:"amount"`
Fee float64 `json:"fee"`
Side string `json:"side"`
Tag string `json:"tag"`
ID int64 `json:"id"`
TradeID string `json:"trade_id"`
Symbol string `json:"symbol"`
OrderID string `json:"order_id"`
CreatedAt string `json:"created_at"`
// Order stores information for a single order
type Order struct {
AverageFillPrice float64 `json:"averageFillPrice"`
ClOrderID string `json:"clOrderID"`
Deviation float64 `json:"deviation"`
FillSize float64 `json:"fillSize"`
Message string `json:"message"`
OrderID string `json:"orderID"`
OrderType int `json:"orderType"`
Price float64 `json:"price"`
Side string `json:"side"`
Size float64 `json:"size"`
Status int `json:"status"`
Stealth float64 `json:"stealth"`
StopPrice float64 `json:"stopPrice"`
Symbol string `json:"symbol"`
Timestamp int64 `json:"timestamp"`
Trigger bool `json:"trigger"`
TriggerPrice float64 `json:"triggerPrice"`
}
type wsSub struct {
@@ -220,3 +334,20 @@ type wsOrderUpdate struct {
TriggerPrice float64 `json:"triggerPrice,string"`
Type string `json:"type"`
}
// ErrorResponse contains errors received from API
type ErrorResponse struct {
ErrorCode int `json:"errorCode"`
Message string `json:"message"`
Status int `json:"status"`
}
// OrderSizeLimit holds accepted minimum, maximum, and size increment when submitting new orders
type OrderSizeLimit struct {
MinOrderSize float64
MaxOrderSize float64
MinSizeIncrement float64
}
// orderSizeLimitMap map of OrderSizeLimit per currency
var orderSizeLimitMap sync.Map

View File

@@ -3,6 +3,7 @@ package btse
import (
"errors"
"fmt"
"math"
"strconv"
"strings"
"sync"
@@ -90,6 +91,7 @@ func (b *BTSE) SetDefaults() {
Websocket: true,
RESTCapabilities: protocol.Features{
TickerFetching: true,
TickerBatching: true,
KlineFetching: true,
TradeFetching: true,
OrderbookFetching: true,
@@ -114,14 +116,38 @@ func (b *BTSE) SetDefaults() {
GetOrder: true,
},
WithdrawPermissions: exchange.NoAPIWithdrawalMethods,
Kline: kline.ExchangeCapabilitiesSupported{
DateRanges: true,
Intervals: true,
},
},
Enabled: exchange.FeaturesEnabled{
AutoPairUpdates: true,
Kline: kline.ExchangeCapabilitiesEnabled{
Intervals: map[string]bool{
kline.OneMin.Word(): true,
kline.ThreeMin.Word(): true,
kline.FiveMin.Word(): true,
kline.FifteenMin.Word(): true,
kline.ThirtyMin.Word(): true,
kline.OneHour.Word(): true,
kline.TwoHour.Word(): true,
kline.FourHour.Word(): true,
kline.SixHour.Word(): true,
kline.TwelveHour.Word(): true,
kline.OneDay.Word(): true,
kline.ThreeDay.Word(): true,
kline.OneWeek.Word(): true,
kline.OneMonth.Word(): true,
},
ResultLimit: 300,
},
},
}
b.Requester = request.New(b.Name,
common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout))
common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout),
request.WithLimiter(SetRateLimit()))
b.API.Endpoints.URLDefault = btseAPIURL
b.API.Endpoints.URL = b.API.Endpoints.URLDefault
@@ -162,6 +188,11 @@ func (b *BTSE) Setup(exch *config.ExchangeConfig) error {
return err
}
err = b.seedOrderSizeLimits()
if err != nil {
return err
}
return b.Websocket.SetupNewConnection(stream.ConnectionSetup{
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
ResponseMaxLimit: exch.WebsocketResponseMaxLimit,
@@ -197,32 +228,17 @@ func (b *BTSE) Run() {
// FetchTradablePairs returns a list of the exchanges tradable pairs
func (b *BTSE) FetchTradablePairs(a asset.Item) ([]string, error) {
var currencies []string
if a == asset.Spot {
m, err := b.GetSpotMarkets()
if err != nil {
return nil, err
}
for x := range m {
if m[x].Status != "active" {
continue
}
currencies = append(currencies, m[x].Symbol)
}
} else if a == asset.Futures {
m, err := b.GetFuturesMarkets()
if err != nil {
return nil, err
}
for x := range m {
if !m[x].Active {
continue
}
currencies = append(currencies, m[x].Symbol)
}
m, err := b.GetMarketSummary("", a == asset.Spot)
if err != nil {
return nil, err
}
for x := range m {
if !m[x].Active {
continue
}
currencies = append(currencies, m[x].Symbol)
}
return currencies, nil
}
@@ -251,41 +267,32 @@ func (b *BTSE) UpdateTradablePairs(forceUpdate bool) error {
// UpdateTicker updates and returns the ticker for a currency pair
func (b *BTSE) UpdateTicker(p currency.Pair, assetType asset.Item) (*ticker.Price, error) {
if assetType == asset.Futures {
// Futures REST implementation needs to be done before this can be
// removed
return nil, common.ErrNotYetImplemented
}
fpair, err := b.FormatExchangeCurrency(p, assetType)
tickers, err := b.GetMarketSummary("", assetType == asset.Spot)
if err != nil {
return nil, err
}
for x := range tickers {
var pair currency.Pair
pair, err = currency.NewPairFromString(tickers[x].Symbol)
if err != nil {
return nil, err
}
t, err := b.GetTicker(fpair.String())
if err != nil {
return nil, err
err = ticker.ProcessTicker(&ticker.Price{
Pair: pair,
Ask: tickers[x].LowestAsk,
Bid: tickers[x].HighestBid,
Low: tickers[x].Low24Hr,
Last: tickers[x].Last,
Volume: tickers[x].Volume,
High: tickers[x].High24Hr,
ExchangeName: b.Name,
AssetType: assetType})
if err != nil {
return nil, err
}
}
s, err := b.GetMarketStatistics(fpair.String())
if err != nil {
return nil, err
}
err = ticker.ProcessTicker(&ticker.Price{
Pair: p,
Ask: t.Ask,
Bid: t.Bid,
Low: s.Low,
Last: t.Price,
Volume: s.Volume,
High: s.High,
LastUpdated: s.Time,
ExchangeName: b.Name,
AssetType: assetType})
if err != nil {
return nil, err
}
return ticker.GetTicker(b.Name, p, assetType)
}
@@ -309,17 +316,11 @@ func (b *BTSE) FetchOrderbook(p currency.Pair, assetType asset.Item) (*orderbook
// UpdateOrderbook updates and returns the orderbook for a currency pair
func (b *BTSE) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderbook.Base, error) {
if assetType == asset.Futures {
// Futures REST implementation needs to be done before this can be
// removed
return nil, common.ErrNotYetImplemented
}
fpair, err := b.FormatExchangeCurrency(p, assetType)
fPair, err := b.FormatExchangeCurrency(p, assetType)
if err != nil {
return nil, err
}
a, err := b.FetchOrderBook(fpair.String())
a, err := b.FetchOrderBook(fPair.String(), 0, 0, 0, assetType == asset.Spot)
if err != nil {
return nil, err
}
@@ -349,7 +350,7 @@ func (b *BTSE) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderboo
// BTSE exchange
func (b *BTSE) UpdateAccountInfo() (account.Holdings, error) {
var a account.Holdings
balance, err := b.GetAccountBalance()
balance, err := b.GetWalletInformation()
if err != nil {
return a, err
}
@@ -397,7 +398,47 @@ func (b *BTSE) GetFundingHistory() ([]exchange.FundHistory, error) {
// GetExchangeHistory returns historic trade data within the timeframe provided.
func (b *BTSE) GetExchangeHistory(p currency.Pair, assetType asset.Item, timestampStart, timestampEnd time.Time) ([]exchange.TradeHistory, error) {
return nil, common.ErrNotYetImplemented
if assetType != asset.Spot {
return nil, common.ErrNotYetImplemented
}
fPair, err := b.FormatExchangeCurrency(p, assetType)
if err != nil {
return nil, err
}
trades, err := b.GetTrades(fPair.String(),
timestampStart, timestampEnd,
0, 0, 0,
false)
if err != nil {
return nil, err
}
var resp []exchange.TradeHistory
for x := range trades {
tempExch := exchange.TradeHistory{
Timestamp: time.Unix(0, trades[x].Time*int64(time.Millisecond)),
Price: trades[x].Price,
Amount: trades[x].Amount,
Exchange: b.Name,
Side: trades[x].Side,
Type: trades[x].Type,
TID: strconv.Itoa(trades[x].SerialID),
}
resp = append(resp, tempExch)
}
return resp, nil
}
func (b *BTSE) withinLimits(pair currency.Pair, amount float64) bool {
val, found := OrderSizeLimits(pair.String())
if !found {
return false
}
return (math.Mod(amount, val.MinSizeIncrement) == 0) ||
amount < val.MinOrderSize ||
amount > val.MaxOrderSize
}
// SubmitOrder submits a new order
@@ -407,26 +448,28 @@ func (b *BTSE) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) {
return resp, err
}
fpair, err := b.FormatExchangeCurrency(s.Pair, s.AssetType)
fPair, err := b.FormatExchangeCurrency(s.Pair, s.AssetType)
if err != nil {
return resp, err
}
inLimits := b.withinLimits(fPair, s.Amount)
if !inLimits {
return resp, errors.New("order outside of limits")
}
r, err := b.CreateOrder(s.ClientID, 0.0,
false,
s.Price, s.Side.String(), s.Amount, 0, 0,
fPair.String(), goodTillCancel,
0.0, s.TriggerPrice,
"", s.Type.String())
if err != nil {
return resp, err
}
r, err := b.CreateOrder(s.Amount,
s.Price,
s.Side.String(),
s.Type.String(),
fpair.String(),
goodTillCancel,
s.ClientID)
if err != nil {
return resp, err
}
resp.IsOrderPlaced = true
resp.OrderID = r[0].OrderID
if *r != "" {
resp.IsOrderPlaced = true
resp.OrderID = *r
}
if s.Type == order.Market {
resp.FullyMatched = true
}
@@ -441,24 +484,17 @@ 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 {
fpair, err := b.FormatExchangeCurrency(order.Pair,
fPair, err := b.FormatExchangeCurrency(order.Pair,
order.AssetType)
if err != nil {
return err
}
r, err := b.CancelExistingOrder(order.ID, fpair.String())
_, err = b.CancelExistingOrder(order.ID, fPair.String(), order.ClientOrderID)
if err != nil {
return err
}
switch r.Code {
case -1:
return errors.New("order cancellation unsuccessful")
case 4:
return errors.New("order cancellation timeout")
}
return nil
}
@@ -467,50 +503,39 @@ func (b *BTSE) CancelOrder(order *order.Cancel) error {
// If not specified, all orders of all markets will be cancelled
func (b *BTSE) CancelAllOrders(orderCancellation *order.Cancel) (order.CancelAllResponse, error) {
var resp order.CancelAllResponse
markets, err := b.GetSpotMarkets()
fPair, err := b.FormatExchangeCurrency(orderCancellation.Pair,
orderCancellation.AssetType)
if err != nil {
return resp, err
}
format, err := b.GetPairFormat(orderCancellation.AssetType, false)
allOrders, err := b.CancelExistingOrder("", fPair.String(), "")
if err != nil {
return resp, err
return resp, nil
}
resp.Status = make(map[string]string)
for x := range markets {
fair, err := b.FormatExchangeCurrency(orderCancellation.Pair,
orderCancellation.AssetType)
if err != nil {
return resp, err
}
checkPair := currency.NewPairWithDelimiter(markets[x].BaseCurrency,
markets[x].QuoteCurrency,
format.Delimiter).String()
if fair.String() != 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.Status[orders[y].Order.ID] = success
}
for x := range allOrders {
if allOrders[x].Status == orderCancelled {
resp.Status[allOrders[x].OrderID] = order.Cancelled.String()
}
}
return resp, nil
}
func orderIntToType(i int) order.Type {
if i == 77 {
return order.Market
} else if i == 76 {
return order.Limit
}
return order.UnknownType
}
// GetOrderInfo returns information on a current open order
func (b *BTSE) GetOrderInfo(orderID string) (order.Detail, error) {
o, err := b.GetOrders("")
o, err := b.GetOrders("", orderID, "")
if err != nil {
return order.Detail{}, err
}
@@ -526,7 +551,7 @@ func (b *BTSE) GetOrderInfo(orderID string) (order.Detail, error) {
}
for i := range o {
if o[i].ID != orderID {
if o[i].OrderID != orderID {
continue
}
@@ -544,39 +569,41 @@ func (b *BTSE) GetOrderInfo(orderID string) (order.Detail, error) {
err)
}
od.Exchange = b.Name
od.Amount = o[i].Amount
od.ID = o[i].ID
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.Amount = o[i].Size
od.ID = o[i].OrderID
od.Date = time.Unix(o[i].Timestamp, 0)
od.Side = side
od.Type = order.Type(strings.ToUpper(o[i].Type))
od.Price = o[i].Price
od.Status = order.Status(o[i].Status)
fills, err := b.GetFills(orderID, "", "", "", "", "")
od.Type = orderIntToType(o[i].OrderType)
od.Price = o[i].Price
od.Status = order.Status(o[i].OrderState)
th, err := b.TradeHistory("",
time.Time{}, time.Time{},
0, 0, 0,
false,
"", orderID)
if err != nil {
return od,
fmt.Errorf("unable to get order fills for orderID %s",
orderID)
}
for i := range fills {
createdAt, err := parseOrderTime(fills[i].CreatedAt)
for i := range th {
createdAt, err := parseOrderTime(th[i].TradeID)
if err != nil {
log.Errorf(log.ExchangeSys,
"%s GetOrderInfo unable to parse time: %s\n", b.Name, err)
}
od.Trades = append(od.Trades, order.TradeHistory{
Timestamp: createdAt,
TID: strconv.FormatInt(fills[i].ID, 10),
Price: fills[i].Price,
Amount: fills[i].Amount,
TID: th[i].TradeID,
Price: th[i].Price,
Amount: th[i].Size,
Exchange: b.Name,
Side: order.Side(fills[i].Side),
Fee: fills[i].Fee,
Side: order.Side(th[i].Side),
Fee: th[i].FeeAmount,
})
}
}
@@ -585,13 +612,38 @@ func (b *BTSE) GetOrderInfo(orderID string) (order.Detail, error) {
// GetDepositAddress returns a deposit address for a specified currency
func (b *BTSE) GetDepositAddress(cryptocurrency currency.Code, accountID string) (string, error) {
return "", common.ErrFunctionNotSupported
address, err := b.GetWalletAddress(cryptocurrency.String())
if err != nil {
return "", err
}
if len(address) == 0 {
addressCreate, err := b.CreateWalletAddress(cryptocurrency.String())
if err != nil {
return "", err
}
if len(addressCreate) != 0 {
return addressCreate[0].Address, nil
}
return "", errors.New("address not found")
}
return address[0].Address, nil
}
// WithdrawCryptocurrencyFunds returns a withdrawal ID when a withdrawal is
// submitted
func (b *BTSE) WithdrawCryptocurrencyFunds(withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) {
return nil, common.ErrFunctionNotSupported
amountToString := strconv.FormatFloat(withdrawRequest.Amount, 'f', 8, 64)
resp, err := b.WalletWithdrawal(withdrawRequest.Currency.String(),
withdrawRequest.Crypto.Address,
withdrawRequest.Crypto.AddressTag,
amountToString)
if err != nil {
return nil, err
}
return &withdraw.ExchangeResponse{
Name: b.Name,
ID: resp.WithdrawID,
}, nil
}
// WithdrawFiatFunds returns a withdrawal ID when a withdrawal is
@@ -608,80 +660,92 @@ func (b *BTSE) WithdrawFiatFundsToInternationalBank(withdrawRequest *withdraw.Re
// GetActiveOrders retrieves any orders that are active/open
func (b *BTSE) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, error) {
resp, err := b.GetOrders("")
if err != nil {
return nil, err
}
format, err := b.GetPairFormat(asset.Spot, false)
if err != nil {
return nil, err
if len(req.Pairs) == 0 {
return nil, errors.New("no pair provided")
}
var orders []order.Detail
for i := range resp {
var side = order.Buy
if strings.EqualFold(resp[i].Side, order.Ask.String()) {
side = order.Sell
}
tm, err := parseOrderTime(resp[i].CreatedAt)
for x := range req.Pairs {
formattedPair, err := b.FormatExchangeCurrency(req.Pairs[x], asset.Spot)
if err != nil {
log.Errorf(log.ExchangeSys,
"%s GetActiveOrders unable to parse time: %s\n",
b.Name,
err)
return nil, err
}
p, err := currency.NewPairDelimiter(resp[i].Symbol,
format.Delimiter)
resp, err := b.GetOrders(formattedPair.String(), "", "")
if err != nil {
log.Errorf(log.ExchangeSys,
"%s GetActiveOrders unable to parse currency pair: %s\n",
b.Name,
err)
return nil, err
}
openOrder := order.Detail{
Pair: p,
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, "", "", "", "", "")
format, err := b.GetPairFormat(asset.Spot, false)
if err != nil {
log.Errorf(log.ExchangeSys,
"%s: Unable to get order fills for orderID %s",
b.Name,
resp[i].ID)
continue
return nil, err
}
for i := range fills {
createdAt, err := parseOrderTime(fills[i].CreatedAt)
for i := range resp {
var side = order.Buy
if strings.EqualFold(resp[i].Side, order.Ask.String()) {
side = order.Sell
}
p, err := currency.NewPairDelimiter(resp[i].Symbol,
format.Delimiter)
if err != nil {
log.Errorf(log.ExchangeSys,
"%s GetActiveOrders unable to parse time: %s\n",
"%s GetActiveOrders unable to parse currency pair: %s\n",
b.Name,
err)
}
openOrder.Trades = append(openOrder.Trades, order.TradeHistory{
Timestamp: createdAt,
TID: strconv.FormatInt(fills[i].ID, 10),
Price: fills[i].Price,
Amount: fills[i].Amount,
Exchange: b.Name,
Side: order.Side(fills[i].Side),
Fee: fills[i].Fee,
})
openOrder := order.Detail{
Pair: p,
Exchange: b.Name,
Amount: resp[i].Size,
ID: resp[i].OrderID,
Date: time.Unix(resp[i].Timestamp, 0),
Side: side,
Price: resp[i].Price,
Status: order.Status(resp[i].OrderState),
}
if resp[i].OrderType == 77 {
openOrder.Type = order.Market
} else if resp[i].OrderType == 76 {
openOrder.Type = order.Limit
}
fills, err := b.TradeHistory(
"",
time.Time{}, time.Time{},
0, 0, 0,
false,
"", resp[i].OrderID)
if err != nil {
log.Errorf(log.ExchangeSys,
"%s: Unable to get order fills for orderID %s",
b.Name,
resp[i].OrderID)
continue
}
for i := range fills {
createdAt, err := parseOrderTime(fills[i].Timestamp)
if err != nil {
log.Errorf(log.ExchangeSys,
"%s GetActiveOrders unable to parse time: %s\n",
b.Name,
err)
}
openOrder.Trades = append(openOrder.Trades, order.TradeHistory{
Timestamp: createdAt,
TID: fills[i].TradeID,
Price: fills[i].Price,
Amount: fills[i].Size,
Exchange: b.Name,
Side: order.Side(fills[i].Side),
Fee: fills[i].FeeAmount,
})
}
orders = append(orders, openOrder)
}
orders = append(orders, openOrder)
}
order.FilterOrdersByType(&orders, req.Type)
@@ -690,10 +754,60 @@ func (b *BTSE) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, err
return orders, nil
}
func matchType(input int, required order.Type) bool {
if (required == order.AnyType) || (input == 76 && required == order.Limit) || input == 77 && required == order.Market {
return true
}
return false
}
// GetOrderHistory retrieves account order information
// Can Limit response to specific order status
func (b *BTSE) GetOrderHistory(getOrdersRequest *order.GetOrdersRequest) ([]order.Detail, error) {
return nil, common.ErrFunctionNotSupported
var resp []order.Detail
if len(getOrdersRequest.Pairs) == 0 {
var err error
getOrdersRequest.Pairs, err = b.GetEnabledPairs(asset.Spot)
if err != nil {
return nil, err
}
}
orderDeref := *getOrdersRequest
for x := range orderDeref.Pairs {
fPair, err := b.FormatExchangeCurrency(orderDeref.Pairs[x], asset.Spot)
if err != nil {
return nil, err
}
currentOrder, err := b.GetOrders(fPair.String(), "", "")
if err != nil {
return nil, err
}
for y := range currentOrder {
if !matchType(currentOrder[y].OrderType, orderDeref.Type) {
continue
}
tempOrder := order.Detail{
Price: currentOrder[y].Price,
Amount: currentOrder[y].Size,
Side: order.Side(currentOrder[y].Side),
Pair: orderDeref.Pairs[x],
}
switch currentOrder[x].OrderState {
case "STATUS_ACTIVE":
tempOrder.Status = order.Active
case "ORDER_CANCELLED":
tempOrder.Status = order.Cancelled
case "ORDER_FULLY_TRANSACTED":
tempOrder.Status = order.Filled
case "ORDER_PARTIALLY_TRANSACTED":
tempOrder.Status = order.PartiallyFilled
default:
tempOrder.Status = order.UnknownStatus
}
resp = append(resp, tempOrder)
}
}
return resp, nil
}
// GetFeeByType returns an estimate of fee based on type of transaction
@@ -712,12 +826,152 @@ func (b *BTSE) ValidateCredentials() error {
return b.CheckTransientError(err)
}
// FormatExchangeKlineInterval formats kline interval to exchange requested type
func (b *BTSE) FormatExchangeKlineInterval(in kline.Interval) string {
return strconv.FormatFloat(in.Duration().Minutes(), 'f', 0, 64)
}
// GetHistoricCandles returns candles between a time period for a set time interval
func (b *BTSE) GetHistoricCandles(pair currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) {
return kline.Item{}, common.ErrFunctionNotSupported
if err := b.ValidateKline(pair, a, interval); err != nil {
return kline.Item{}, err
}
fPair, err := b.FormatExchangeCurrency(pair, a)
if err != nil {
return kline.Item{}, err
}
intervalInt, err := strconv.Atoi(b.FormatExchangeKlineInterval(interval))
if err != nil {
return kline.Item{}, err
}
klineRet := kline.Item{
Exchange: b.Name,
Pair: fPair,
Asset: a,
Interval: interval,
}
switch a {
case asset.Spot:
req, err := b.OHLCV(fPair.String(),
start,
end,
intervalInt)
if err != nil {
return kline.Item{}, err
}
for x := range req {
klineRet.Candles = append(klineRet.Candles, kline.Candle{
Time: time.Unix(int64(req[x][0]), 0),
Open: req[x][1],
High: req[x][2],
Low: req[x][3],
Close: req[x][4],
Volume: req[x][5],
})
}
case asset.Futures:
return kline.Item{}, common.ErrNotYetImplemented
default:
return kline.Item{}, fmt.Errorf("asset %v not supported", a.String())
}
klineRet.SortCandlesByTimestamp(false)
return klineRet, nil
}
// GetHistoricCandlesExtended returns candles between a time period for a set time interval
func (b *BTSE) GetHistoricCandlesExtended(pair currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) {
return kline.Item{}, common.ErrFunctionNotSupported
if err := b.ValidateKline(pair, a, interval); err != nil {
return kline.Item{}, err
}
if kline.TotalCandlesPerInterval(start, end, interval) > b.Features.Enabled.Kline.ResultLimit {
return kline.Item{}, errors.New(kline.ErrRequestExceedsExchangeLimits)
}
fPair, err := b.FormatExchangeCurrency(pair, a)
if err != nil {
return kline.Item{}, err
}
intervalInt, err := strconv.Atoi(b.FormatExchangeKlineInterval(interval))
if err != nil {
return kline.Item{}, err
}
klineRet := kline.Item{
Exchange: b.Name,
Pair: fPair,
Asset: a,
Interval: interval,
}
switch a {
case asset.Spot:
req, err := b.OHLCV(fPair.String(),
start,
end,
intervalInt)
if err != nil {
return kline.Item{}, err
}
for x := range req {
klineRet.Candles = append(klineRet.Candles, kline.Candle{
Time: time.Unix(int64(req[x][0]), 0),
Open: req[x][1],
High: req[x][2],
Low: req[x][3],
Close: req[x][4],
Volume: req[x][5],
})
}
case asset.Futures:
return kline.Item{}, common.ErrNotYetImplemented
default:
return kline.Item{}, fmt.Errorf("asset %v not supported", a.String())
}
klineRet.SortCandlesByTimestamp(false)
return klineRet, nil
}
func (b *BTSE) seedOrderSizeLimits() error {
pairs, err := b.GetMarketSummary("", true)
if err != nil {
return err
}
for x := range pairs {
tempValues := OrderSizeLimit{
MinOrderSize: pairs[x].MinOrderSize,
MaxOrderSize: pairs[x].MaxOrderSize,
MinSizeIncrement: pairs[x].MinSizeIncrement,
}
orderSizeLimitMap.Store(pairs[x].Symbol, tempValues)
}
pairs, err = b.GetMarketSummary("", false)
if err != nil {
return err
}
for x := range pairs {
tempValues := OrderSizeLimit{
MinOrderSize: pairs[x].MinOrderSize,
MaxOrderSize: pairs[x].MaxOrderSize,
MinSizeIncrement: pairs[x].MinSizeIncrement,
}
orderSizeLimitMap.Store(pairs[x].Symbol, tempValues)
}
return nil
}
// OrderSizeLimits looks up currency pair in orderSizeLimitMap and returns OrderSizeLimit
func OrderSizeLimits(pair string) (limits OrderSizeLimit, found bool) {
resp, ok := orderSizeLimitMap.Load(pair)
if !ok {
return
}
val, ok := resp.(OrderSizeLimit)
return val, ok
}

View File

@@ -0,0 +1,42 @@
package btse
import (
"time"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"golang.org/x/time/rate"
)
const (
btseRateInterval = time.Second
btseQueryLimit = 15
btseOrdersLimit = 75
queryFunc request.EndpointLimit = iota
orderFunc
)
// RateLimit implements the request.Limiter interface
type RateLimit struct {
Query *rate.Limiter
Orders *rate.Limiter
}
// Limit executes rate limiting functionality for exchange
func (r *RateLimit) Limit(f request.EndpointLimit) error {
switch f {
case orderFunc:
time.Sleep(r.Orders.Reserve().Delay())
default:
time.Sleep(r.Query.Reserve().Delay())
}
return nil
}
// SetRateLimit returns the rate limit for the exchange
func SetRateLimit() *RateLimit {
return &RateLimit{
Orders: request.NewRateLimit(btseRateInterval, btseOrdersLimit),
Query: request.NewRateLimit(btseRateInterval, btseQueryLimit),
}
}

View File

@@ -291,9 +291,9 @@ func CalcDateRanges(start, end time.Time, interval Interval, limit uint32) (out
}
// SortCandlesByTimestamp sorts candles by timestamp
func (k *Item) SortCandlesByTimestamp(asc bool) {
func (k *Item) SortCandlesByTimestamp(desc bool) {
sort.Slice(k.Candles, func(i, j int) bool {
if asc {
if desc {
return k.Candles[i].Time.After(k.Candles[j].Time)
}
return k.Candles[i].Time.Before(k.Candles[j].Time)