mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-13 15:09:42 +00:00
Bittrex: Update API v1.1 to v3 and add websocket support (#646)
* Update Bittrex API from v1.1 to v3 V1.1 has been retired as of 9/30/2020 - Update REST to V3 - Add initial websocket support * Bittrex update - enable websockets in testdata config file * Update Bittrex - add Websocket capability to docs * Update Bittrex connector - AppVeyor warnings - Update tests - Generate documentation - Fix nits * Update Bittrex - add websocket order processing * Update Bittrex connector * Bittrex connector - fix ineffectual err assignment * Fix nits * Orderbook synchronization * Remove redundant nil * Log WS fetch orderbook message as debug message instead of as warning * Update after rebase * Add tests * Add allowed candle interval values * Replace literals with declared constants * Replace variable name 'request' with 'req' * Add check and update for deprecated REST URL * Nits and some cleaning up * Change ParseInt bit size to 64 * [FIX] Remove several shadow declarations * Do not export constructTicker * Remove parseTime() * Update GetHistoricCandles() * [FIX] Address gocritic nits * [FIX] Address gocritic nits * Use SendMessageReturnResponse() instead of local map * Rate limit subscribing and unsubscribing * [FIX] use go routine for subscribing and unsubscribing * [FIX] Set correct index for map * [FIX] Address unused vars, literals, time format * Adjusted timing when subscribing to many order books * Cache partial updates to tickers instead of calling REST function * [FIX] Update sequence nr when multiple updates are queued * Address golint issues * Fix nits
This commit is contained in:
@@ -25,7 +25,7 @@ Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader
|
||||
| Bithumb | Yes | NA | NA |
|
||||
| BitMEX | Yes | Yes | NA |
|
||||
| Bitstamp | Yes | Yes | No |
|
||||
| Bittrex | Yes | No | NA |
|
||||
| Bittrex | Yes | Yes | NA |
|
||||
| BTCMarkets | Yes | Yes | NA |
|
||||
| BTSE | Yes | Yes | NA |
|
||||
| CoinbasePro | Yes | Yes | No|
|
||||
|
||||
@@ -6,6 +6,11 @@
|
||||
|
||||
+ REST Support
|
||||
|
||||
### Notes
|
||||
|
||||
- Bittrex used to have reversed market names: btc-ltc. The v3 API changed this to the more widely accepted format with first the base pair and then the quote pair: ltc-btc.
|
||||
- Asset names and market names are not case sensitive.
|
||||
|
||||
### How to enable
|
||||
|
||||
+ [Enable via configuration](https://github.com/thrasher-corp/gocryptotrader/tree/master/config#enable-exchange-via-config-example)
|
||||
|
||||
@@ -48,7 +48,7 @@ _b in this context is an `IBotExchange` implemented struct_
|
||||
| Bithumb | Yes | NA | No |
|
||||
| BitMEX | Yes | Yes | Yes |
|
||||
| Bitstamp | Yes | Yes | No |
|
||||
| Bittrex | Yes | No | No |
|
||||
| Bittrex | Yes | Yes | No |
|
||||
| BTCMarkets | Yes | Yes | No |
|
||||
| BTSE | Yes | Yes | No |
|
||||
| Coinbene | Yes | Yes | No |
|
||||
|
||||
@@ -26,7 +26,7 @@ Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader
|
||||
| Bithumb | Yes | NA | NA |
|
||||
| BitMEX | Yes | Yes | NA |
|
||||
| Bitstamp | Yes | Yes | No |
|
||||
| Bittrex | Yes | No | NA |
|
||||
| Bittrex | Yes | Yes | NA |
|
||||
| BTCMarkets | Yes | Yes | NA |
|
||||
| BTSE | Yes | Yes | NA |
|
||||
| CoinbasePro | Yes | Yes | No|
|
||||
|
||||
@@ -202,7 +202,7 @@ Yes means supported, No means not yet implemented and NA means protocol unsuppor
|
||||
| Bithumb | Yes | NA | NA |
|
||||
| BitMEX | Yes | Yes | NA |
|
||||
| Bitstamp | Yes | Yes | No |
|
||||
| Bittrex | Yes | No | NA |
|
||||
| Bittrex | Yes | Yes | NA |
|
||||
| BTCMarkets | Yes | No | NA |
|
||||
| BTSE | Yes | Yes | NA |
|
||||
| COINUT | Yes | Yes | NA |
|
||||
|
||||
@@ -24,6 +24,11 @@ Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader
|
||||
|
||||
+ REST Support
|
||||
|
||||
### Notes
|
||||
|
||||
- Bittrex used to have reversed market names: btc-ltc. The v3 API changed this to the more widely accepted format with first the base pair and then the quote pair: ltc-btc.
|
||||
- Asset names and market names are not case sensitive.
|
||||
|
||||
### How to enable
|
||||
|
||||
+ [Enable via configuration](https://github.com/thrasher-corp/gocryptotrader/tree/master/config#enable-exchange-via-config-example)
|
||||
|
||||
@@ -1,425 +1,366 @@
|
||||
package bittrex
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"encoding/json"
|
||||
"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/request"
|
||||
)
|
||||
|
||||
const (
|
||||
spotURL = "spotAPIURL"
|
||||
spotWSURL = "spotWSURL"
|
||||
bittrexAPIURL = "https://bittrex.com/api/v1.1"
|
||||
bittrexAPIVersion = "v1.1"
|
||||
bittrexMaxOpenOrders = 500
|
||||
bittrexMaxOrderCountPerDay = 200000
|
||||
|
||||
// Returned messages from Bittrex API
|
||||
bittrexAddressGenerating = "ADDRESS_GENERATING"
|
||||
bittrexErrorMarketNotProvided = "MARKET_NOT_PROVIDED"
|
||||
bittrexErrorInvalidMarket = "INVALID_MARKET"
|
||||
bittrexErrorAPIKeyInvalid = "APIKEY_INVALID"
|
||||
bittrexErrorInvalidPermission = "INVALID_PERMISSION"
|
||||
|
||||
// Public requests
|
||||
bittrexAPIGetMarkets = "public/getmarkets"
|
||||
bittrexAPIGetCurrencies = "public/getcurrencies"
|
||||
bittrexAPIGetTicker = "public/getticker"
|
||||
bittrexAPIGetMarketSummaries = "public/getmarketsummaries"
|
||||
bittrexAPIGetMarketSummary = "public/getmarketsummary"
|
||||
bittrexAPIGetOrderbook = "public/getorderbook"
|
||||
bittrexAPIGetMarketHistory = "public/getmarkethistory"
|
||||
|
||||
// Market requests
|
||||
bittrexAPIBuyLimit = "market/buylimit"
|
||||
bittrexAPISellLimit = "market/selllimit"
|
||||
bittrexAPICancel = "market/cancel"
|
||||
bittrexAPIGetOpenOrders = "market/getopenorders"
|
||||
|
||||
// Account requests
|
||||
bittrexAPIGetBalances = "account/getbalances"
|
||||
bittrexAPIGetBalance = "account/getbalance"
|
||||
bittrexAPIGetDepositAddress = "account/getdepositaddress"
|
||||
bittrexAPIWithdraw = "account/withdraw"
|
||||
bittrexAPIGetOrder = "account/getorder"
|
||||
bittrexAPIGetOrderHistory = "account/getorderhistory"
|
||||
bittrexAPIGetWithdrawalHistory = "account/getwithdrawalhistory"
|
||||
bittrexAPIGetDepositHistory = "account/getdeposithistory"
|
||||
|
||||
bittrexRateInterval = time.Minute
|
||||
bittrexRequestRate = 60
|
||||
bittrexTimeLayout = "2006-01-02T15:04:05"
|
||||
)
|
||||
|
||||
// Bittrex is the overaching type across the bittrex methods
|
||||
type Bittrex struct {
|
||||
exchange.Base
|
||||
WsSequenceOrders int64
|
||||
|
||||
obm *orderbookManager
|
||||
tickerCache *TickerCache
|
||||
}
|
||||
|
||||
const (
|
||||
bittrexAPIRestURL = "https://api.bittrex.com/v3"
|
||||
bittrexAPIDeprecatedURL = "https://bittrex.com/api/v1.1"
|
||||
|
||||
// Public endpoints
|
||||
getMarkets = "/markets"
|
||||
getMarketSummaries = "/markets/summaries"
|
||||
getTicker = "/markets/%s/ticker"
|
||||
getMarketSummary = "/markets/%s/summary"
|
||||
getMarketTrades = "/markets/%s/trades"
|
||||
getOrderbook = "/markets/%s/orderbook?depth=%s"
|
||||
getRecentCandles = "/markets/%s/candles/%s/%s/recent"
|
||||
getHistoricalCandles = "/markets/%s/candles/%s/%s/historical/%s"
|
||||
getCurrencies = "/currencies"
|
||||
|
||||
// Authenticated endpoints
|
||||
getBalances = "/balances"
|
||||
getBalance = "/balances/%s"
|
||||
getDepositAddress = "/addresses/%s"
|
||||
getAllOpenOrders = "/orders/open"
|
||||
getOpenOrders = "/orders/open?marketSymbol=%s"
|
||||
getOrder = "/orders/%s"
|
||||
getClosedOrders = "/orders/closed?marketSymbol=%s"
|
||||
cancelOrder = "/orders/%s"
|
||||
cancelOpenOrders = "/orders/open"
|
||||
getClosedWithdrawals = "/withdrawals/closed"
|
||||
getOpenWithdrawals = "/withdrawals/open"
|
||||
submitWithdrawal = "/transfers"
|
||||
getClosedDeposits = "/deposits/closed"
|
||||
getOpenDeposits = "/deposits/open"
|
||||
submitOrder = "/orders"
|
||||
|
||||
// Other Consts
|
||||
ratePeriod = time.Minute
|
||||
rateLimit = 60
|
||||
orderbookDepth = 500 // ws uses REST snapshots and needs identical depths
|
||||
)
|
||||
|
||||
// GetMarkets is used to get the open and available trading markets at Bittrex
|
||||
// along with other meta data.
|
||||
func (b *Bittrex) GetMarkets() (Market, error) {
|
||||
var markets Market
|
||||
|
||||
if err := b.SendHTTPRequest(exchange.RestSpot, "/"+bittrexAPIGetMarkets+"/", &markets); err != nil {
|
||||
return markets, err
|
||||
}
|
||||
|
||||
if !markets.Success {
|
||||
return markets, errors.New(markets.Message)
|
||||
}
|
||||
return markets, nil
|
||||
func (b *Bittrex) GetMarkets() ([]MarketData, error) {
|
||||
var resp []MarketData
|
||||
return resp, b.SendHTTPRequest(exchange.RestSpot, getMarkets, &resp, nil)
|
||||
}
|
||||
|
||||
// GetCurrencies is used to get all supported currencies at Bittrex
|
||||
func (b *Bittrex) GetCurrencies() (Currency, error) {
|
||||
var currencies Currency
|
||||
|
||||
if err := b.SendHTTPRequest(exchange.RestSpot, "/"+bittrexAPIGetCurrencies+"/", ¤cies); err != nil {
|
||||
return currencies, err
|
||||
}
|
||||
|
||||
if !currencies.Success {
|
||||
return currencies, errors.New(currencies.Message)
|
||||
}
|
||||
return currencies, nil
|
||||
func (b *Bittrex) GetCurrencies() ([]CurrencyData, error) {
|
||||
var resp []CurrencyData
|
||||
return resp, b.SendHTTPRequest(exchange.RestSpot, getCurrencies, &resp, nil)
|
||||
}
|
||||
|
||||
// GetTicker sends a public get request and returns current ticker information
|
||||
// on the supplied currency. Example currency input param "btc-ltc".
|
||||
func (b *Bittrex) GetTicker(currencyPair string) (Ticker, error) {
|
||||
tick := Ticker{}
|
||||
path := "/" + bittrexAPIGetTicker + "?market=" + strings.ToUpper(currencyPair)
|
||||
|
||||
if err := b.SendHTTPRequest(exchange.RestSpot, path, &tick); err != nil {
|
||||
return tick, err
|
||||
}
|
||||
|
||||
if !tick.Success {
|
||||
return tick, errors.New(tick.Message)
|
||||
}
|
||||
return tick, nil
|
||||
// on the supplied currency. Example currency input param "ltc-btc".
|
||||
func (b *Bittrex) GetTicker(marketName string) (TickerData, error) {
|
||||
var resp TickerData
|
||||
return resp, b.SendHTTPRequest(exchange.RestSpot, fmt.Sprintf(getTicker, marketName), &resp, nil)
|
||||
}
|
||||
|
||||
// GetMarketSummaries is used to get the last 24 hour summary of all active
|
||||
// exchanges
|
||||
func (b *Bittrex) GetMarketSummaries() (MarketSummary, error) {
|
||||
var summaries MarketSummary
|
||||
|
||||
if err := b.SendHTTPRequest(exchange.RestSpot, "/"+bittrexAPIGetMarketSummaries+"/", &summaries); err != nil {
|
||||
return summaries, err
|
||||
}
|
||||
|
||||
if !summaries.Success {
|
||||
return summaries, errors.New(summaries.Message)
|
||||
}
|
||||
return summaries, nil
|
||||
func (b *Bittrex) GetMarketSummaries() ([]MarketSummaryData, error) {
|
||||
var resp []MarketSummaryData
|
||||
return resp, b.SendHTTPRequest(exchange.RestSpot, getMarketSummaries, &resp, nil)
|
||||
}
|
||||
|
||||
// GetMarketSummary is used to get the last 24 hour summary of all active
|
||||
// exchanges by currency pair (btc-ltc).
|
||||
func (b *Bittrex) GetMarketSummary(currencyPair string) (MarketSummary, error) {
|
||||
var summary MarketSummary
|
||||
if err := b.SendHTTPRequest(exchange.RestSpot, "/"+bittrexAPIGetMarketSummary+"?market="+strings.ToLower(currencyPair), &summary); err != nil {
|
||||
return summary, err
|
||||
}
|
||||
|
||||
if !summary.Success {
|
||||
return summary, errors.New(summary.Message)
|
||||
}
|
||||
return summary, nil
|
||||
// exchanges by currency pair (ltc-btc).
|
||||
func (b *Bittrex) GetMarketSummary(marketName string) (MarketSummaryData, error) {
|
||||
var resp MarketSummaryData
|
||||
return resp, b.SendHTTPRequest(exchange.RestSpot, fmt.Sprintf(getMarketSummary, marketName), &resp, nil)
|
||||
}
|
||||
|
||||
// GetOrderbook method returns current order book information by currency, type
|
||||
// & depth.
|
||||
// "Currency Pair" ie btc-ltc
|
||||
// "Category" either "buy", "sell" or "both"; for ease of use and reduced
|
||||
// complexity this function is set to "both"
|
||||
// "Depth" max depth is 50 but you can literally set it any integer you want and
|
||||
// it returns full depth. So depth default is 50.
|
||||
func (b *Bittrex) GetOrderbook(currencyPair string) (OrderBooks, error) {
|
||||
var orderbooks OrderBooks
|
||||
path := "/" + bittrexAPIGetOrderbook + "?market=" + strings.ToLower(currencyPair) + "&type=both&depth=50"
|
||||
if err := b.SendHTTPRequest(exchange.RestSpot, path, &orderbooks); err != nil {
|
||||
return orderbooks, err
|
||||
// GetOrderbook method returns current order book information by currency and depth.
|
||||
// "marketSymbol" ie ltc-btc
|
||||
// "depth" is either 1, 25 or 500. Server side, the depth defaults to 25.
|
||||
func (b *Bittrex) GetOrderbook(marketName string, depth int64) (OrderbookData, int64, error) {
|
||||
strDepth := strconv.FormatInt(depth, 10)
|
||||
|
||||
var resp OrderbookData
|
||||
var sequence int64
|
||||
resultHeader := http.Header{}
|
||||
err := b.SendHTTPRequest(exchange.RestSpot, fmt.Sprintf(getOrderbook, marketName, strDepth), &resp, &resultHeader)
|
||||
if err != nil {
|
||||
return OrderbookData{}, 0, err
|
||||
}
|
||||
sequence, err = strconv.ParseInt(resultHeader.Get("sequence"), 10, 64)
|
||||
if err != nil {
|
||||
return OrderbookData{}, 0, err
|
||||
}
|
||||
|
||||
if !orderbooks.Success {
|
||||
return orderbooks, errors.New(orderbooks.Message)
|
||||
}
|
||||
return orderbooks, nil
|
||||
return resp, sequence, nil
|
||||
}
|
||||
|
||||
// GetMarketHistory retrieves the latest trades that have occurred for a specific
|
||||
// market
|
||||
func (b *Bittrex) GetMarketHistory(currencyPair string) (MarketHistory, error) {
|
||||
var marketHistoriae MarketHistory
|
||||
path := "/" + bittrexAPIGetMarketHistory + "?market=" + strings.ToUpper(currencyPair)
|
||||
if err := b.SendHTTPRequest(exchange.RestSpot, path, &marketHistoriae); err != nil {
|
||||
return marketHistoriae, err
|
||||
}
|
||||
|
||||
if !marketHistoriae.Success {
|
||||
return marketHistoriae, errors.New(marketHistoriae.Message)
|
||||
}
|
||||
return marketHistoriae, nil
|
||||
// GetMarketHistory retrieves the latest trades that have occurred for a specific market
|
||||
func (b *Bittrex) GetMarketHistory(currency string) ([]TradeData, error) {
|
||||
var resp []TradeData
|
||||
return resp, b.SendHTTPRequest(exchange.RestSpot, fmt.Sprintf(getMarketTrades, currency), &resp, nil)
|
||||
}
|
||||
|
||||
// PlaceBuyLimit is used to place a buy order in a specific market. Use buylimit
|
||||
// to place limit orders. Make sure you have the proper permissions set on your
|
||||
// API keys for this call to work.
|
||||
// "Currency" ie "btc-ltc"
|
||||
// "Quantity" is the amount to purchase
|
||||
// "Rate" is the rate at which to purchase
|
||||
func (b *Bittrex) PlaceBuyLimit(currencyPair string, quantity, rate float64) (UUID, error) {
|
||||
var id UUID
|
||||
values := url.Values{}
|
||||
values.Set("market", currencyPair)
|
||||
values.Set("quantity", strconv.FormatFloat(quantity, 'E', -1, 64))
|
||||
values.Set("rate", strconv.FormatFloat(rate, 'E', -1, 64))
|
||||
if err := b.SendAuthenticatedHTTPRequest(exchange.RestSpot, "/"+bittrexAPIBuyLimit, values, &id); err != nil {
|
||||
return id, err
|
||||
// Order places an order
|
||||
func (b *Bittrex) Order(marketName, side, orderType string, timeInForce TimeInForce, price, amount, ceiling float64) (OrderData, error) {
|
||||
req := make(map[string]interface{})
|
||||
req["marketSymbol"] = marketName
|
||||
req["direction"] = side
|
||||
req["type"] = orderType
|
||||
req["quantity"] = strconv.FormatFloat(amount, 'f', -1, 64)
|
||||
if orderType == "CEILING_LIMIT" || orderType == "CEILING_MARKET" {
|
||||
req["ceiling"] = strconv.FormatFloat(ceiling, 'f', -1, 64)
|
||||
}
|
||||
|
||||
if !id.Success {
|
||||
return id, errors.New(id.Message)
|
||||
if orderType == "LIMIT" {
|
||||
req["limit"] = strconv.FormatFloat(price, 'f', -1, 64)
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// PlaceSellLimit is used to place a sell order in a specific market. Use
|
||||
// selllimit to place limit orders. Make sure you have the proper permissions
|
||||
// set on your API keys for this call to work.
|
||||
// "Currency" ie "btc-ltc"
|
||||
// "Quantity" is the amount to purchase
|
||||
// "Rate" is the rate at which to purchase
|
||||
func (b *Bittrex) PlaceSellLimit(currencyPair string, quantity, rate float64) (UUID, error) {
|
||||
var id UUID
|
||||
values := url.Values{}
|
||||
values.Set("market", currencyPair)
|
||||
values.Set("quantity", strconv.FormatFloat(quantity, 'E', -1, 64))
|
||||
values.Set("rate", strconv.FormatFloat(rate, 'E', -1, 64))
|
||||
if err := b.SendAuthenticatedHTTPRequest(exchange.RestSpot, "/"+bittrexAPISellLimit, values, &id); err != nil {
|
||||
return id, err
|
||||
if timeInForce != "" {
|
||||
req["timeInForce"] = timeInForce
|
||||
} else {
|
||||
req["timeInForce"] = GoodTilCancelled
|
||||
}
|
||||
|
||||
if !id.Success {
|
||||
return id, errors.New(id.Message)
|
||||
}
|
||||
return id, nil
|
||||
var resp OrderData
|
||||
return resp, b.SendAuthHTTPRequest(exchange.RestSpot, http.MethodPost, submitOrder, nil, req, &resp, nil)
|
||||
}
|
||||
|
||||
// GetOpenOrders returns all orders that you currently have opened.
|
||||
// A specific market can be requested for example "btc-ltc"
|
||||
func (b *Bittrex) GetOpenOrders(currencyPair string) (Order, error) {
|
||||
var orders Order
|
||||
values := url.Values{}
|
||||
if !(currencyPair == "" || currencyPair == " ") {
|
||||
values.Set("market", currencyPair)
|
||||
// A specific market can be requested for example "ltc-btc"
|
||||
func (b *Bittrex) GetOpenOrders(marketName string) ([]OrderData, int64, error) {
|
||||
var path string
|
||||
if marketName == "" || marketName == " " {
|
||||
path = getAllOpenOrders
|
||||
} else {
|
||||
path = fmt.Sprintf(getOpenOrders, marketName)
|
||||
}
|
||||
|
||||
if err := b.SendAuthenticatedHTTPRequest(exchange.RestSpot, "/"+bittrexAPIGetOpenOrders, values, &orders); err != nil {
|
||||
return orders, err
|
||||
var resp []OrderData
|
||||
var sequence int64
|
||||
resultHeader := http.Header{}
|
||||
err := b.SendAuthHTTPRequest(exchange.RestSpot, http.MethodGet, path, nil, nil, &resp, &resultHeader)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
if !orders.Success {
|
||||
return orders, errors.New(orders.Message)
|
||||
sequence, err = strconv.ParseInt(resultHeader.Get("sequence"), 10, 64)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return orders, nil
|
||||
return resp, sequence, err
|
||||
}
|
||||
|
||||
// CancelExistingOrder is used to cancel a buy or sell order.
|
||||
func (b *Bittrex) CancelExistingOrder(uuid string) (Balances, error) {
|
||||
var balances Balances
|
||||
values := url.Values{}
|
||||
values.Set("uuid", uuid)
|
||||
|
||||
if err := b.SendAuthenticatedHTTPRequest(exchange.RestSpot, "/"+bittrexAPICancel, values, &balances); err != nil {
|
||||
return balances, err
|
||||
}
|
||||
|
||||
if !balances.Success {
|
||||
return balances, errors.New(balances.Message)
|
||||
}
|
||||
return balances, nil
|
||||
func (b *Bittrex) CancelExistingOrder(uuid string) (OrderData, error) {
|
||||
var resp OrderData
|
||||
return resp, b.SendAuthHTTPRequest(exchange.RestSpot, http.MethodDelete, fmt.Sprintf(cancelOrder, uuid), nil, nil, &resp, nil)
|
||||
}
|
||||
|
||||
// GetAccountBalances is used to retrieve all balances from your account
|
||||
func (b *Bittrex) GetAccountBalances() (Balances, error) {
|
||||
var balances Balances
|
||||
// CancelOpenOrders is used to cancel all open orders for a specific market
|
||||
// Or cancel all orders for all markets if the parameter `markets` is set to ""
|
||||
func (b *Bittrex) CancelOpenOrders(market string) ([]BulkCancelResultData, error) {
|
||||
var resp []BulkCancelResultData
|
||||
|
||||
if err := b.SendAuthenticatedHTTPRequest(exchange.RestSpot, "/"+bittrexAPIGetBalances, url.Values{}, &balances); err != nil {
|
||||
return balances, err
|
||||
params := url.Values{}
|
||||
if market != "" {
|
||||
params.Set("marketSymbol", market)
|
||||
}
|
||||
|
||||
if !balances.Success {
|
||||
return balances, errors.New(balances.Message)
|
||||
return resp, b.SendAuthHTTPRequest(exchange.RestSpot, http.MethodDelete, cancelOpenOrders, params, nil, &resp, nil)
|
||||
}
|
||||
|
||||
// GetRecentCandles retrieves recent candles;
|
||||
// Interval: MINUTE_1, MINUTE_5, HOUR_1, or DAY_1
|
||||
// Type: TRADE or MIDPOINT
|
||||
func (b *Bittrex) GetRecentCandles(marketName, candleInterval, candleType string) ([]CandleData, error) {
|
||||
var resp []CandleData
|
||||
|
||||
return resp, b.SendHTTPRequest(exchange.RestSpot, fmt.Sprintf(getRecentCandles, marketName, candleType, candleInterval), &resp, nil)
|
||||
}
|
||||
|
||||
// GetHistoricalCandles retrieves recent candles
|
||||
// Type: TRADE or MIDPOINT
|
||||
func (b *Bittrex) GetHistoricalCandles(marketName, candleInterval, candleType string, year, month, day int) ([]CandleData, error) {
|
||||
var resp []CandleData
|
||||
|
||||
var start string
|
||||
switch candleInterval {
|
||||
case "MINUTE_1", "MINUTE_5":
|
||||
// Retrieve full day
|
||||
start = fmt.Sprintf("%d/%d/%d", year, month, day)
|
||||
case "HOUR_1":
|
||||
// Retrieve full month
|
||||
start = fmt.Sprintf("%d/%d", year, month)
|
||||
case "DAY_1":
|
||||
// Retrieve full year
|
||||
start = fmt.Sprintf("%d", year)
|
||||
default:
|
||||
return resp, fmt.Errorf("invalid interval %v, not supported", candleInterval)
|
||||
}
|
||||
return balances, nil
|
||||
|
||||
return resp, b.SendHTTPRequest(exchange.RestSpot, fmt.Sprintf(getHistoricalCandles, marketName, candleType, candleInterval, start), &resp, nil)
|
||||
}
|
||||
|
||||
// GetBalances is used to retrieve all balances from your account
|
||||
func (b *Bittrex) GetBalances() ([]BalanceData, error) {
|
||||
var resp []BalanceData
|
||||
return resp, b.SendAuthHTTPRequest(exchange.RestSpot, http.MethodGet, getBalances, nil, nil, &resp, nil)
|
||||
}
|
||||
|
||||
// GetAccountBalanceByCurrency is used to retrieve the balance from your account
|
||||
// for a specific currency. ie. "btc" or "ltc"
|
||||
func (b *Bittrex) GetAccountBalanceByCurrency(currency string) (Balance, error) {
|
||||
var balance Balance
|
||||
values := url.Values{}
|
||||
values.Set("currency", currency)
|
||||
|
||||
if err := b.SendAuthenticatedHTTPRequest(exchange.RestSpot, "/"+bittrexAPIGetBalance, values, &balance); err != nil {
|
||||
return balance, err
|
||||
}
|
||||
|
||||
if !balance.Success {
|
||||
return balance, errors.New(balance.Message)
|
||||
}
|
||||
return balance, nil
|
||||
func (b *Bittrex) GetAccountBalanceByCurrency(currency string) (BalanceData, error) {
|
||||
var resp BalanceData
|
||||
return resp, b.SendAuthHTTPRequest(exchange.RestSpot, http.MethodGet, fmt.Sprintf(getBalance, currency), nil, nil, &resp, nil)
|
||||
}
|
||||
|
||||
// GetCryptoDepositAddress is used to retrieve or generate an address for a specific
|
||||
// currency. If one does not exist, the call will fail and return
|
||||
// ADDRESS_GENERATING until one is available.
|
||||
func (b *Bittrex) GetCryptoDepositAddress(currency string) (DepositAddress, error) {
|
||||
var address DepositAddress
|
||||
values := url.Values{}
|
||||
values.Set("currency", currency)
|
||||
|
||||
if err := b.SendAuthenticatedHTTPRequest(exchange.RestSpot, "/"+bittrexAPIGetDepositAddress, values, &address); err != nil {
|
||||
return address, err
|
||||
}
|
||||
|
||||
if !address.Success {
|
||||
return address, errors.New(address.Message)
|
||||
}
|
||||
return address, nil
|
||||
// GetCryptoDepositAddress is used to retrieve an address for a specific currency
|
||||
func (b *Bittrex) GetCryptoDepositAddress(currency string) (AddressData, error) {
|
||||
var resp AddressData
|
||||
return resp, b.SendAuthHTTPRequest(exchange.RestSpot, http.MethodGet, fmt.Sprintf(getDepositAddress, currency), nil, nil, &resp, nil)
|
||||
}
|
||||
|
||||
// Withdraw is used to withdraw funds from your account.
|
||||
// note: Please account for transaction fee.
|
||||
func (b *Bittrex) Withdraw(currency, paymentID, address string, quantity float64) (UUID, error) {
|
||||
var id UUID
|
||||
values := url.Values{}
|
||||
values.Set("currency", currency)
|
||||
values.Set("quantity", strconv.FormatFloat(quantity, 'f', -1, 64))
|
||||
values.Set("address", address)
|
||||
func (b *Bittrex) Withdraw(currency, paymentID, address string, quantity float64) (WithdrawalData, error) {
|
||||
req := make(map[string]interface{})
|
||||
req["currencySymbol"] = currency
|
||||
req["quantity"] = strconv.FormatFloat(quantity, 'f', -1, 64)
|
||||
req["cryptoAddress"] = address
|
||||
if len(paymentID) > 0 {
|
||||
values.Set("paymentid", paymentID)
|
||||
req["cryptoAddressTag"] = paymentID
|
||||
}
|
||||
|
||||
if err := b.SendAuthenticatedHTTPRequest(exchange.RestSpot, "/"+bittrexAPIWithdraw, values, &id); err != nil {
|
||||
return id, err
|
||||
}
|
||||
|
||||
if !id.Success {
|
||||
return id, errors.New(id.Message)
|
||||
}
|
||||
return id, nil
|
||||
var resp WithdrawalData
|
||||
return resp, b.SendAuthHTTPRequest(exchange.RestSpot, http.MethodPost, submitWithdrawal, nil, req, &resp, nil)
|
||||
}
|
||||
|
||||
// GetOrder is used to retrieve a single order by UUID.
|
||||
func (b *Bittrex) GetOrder(uuid string) (Order, error) {
|
||||
var order Order
|
||||
values := url.Values{}
|
||||
values.Set("uuid", uuid)
|
||||
|
||||
if err := b.SendAuthenticatedHTTPRequest(exchange.RestSpot, "/"+bittrexAPIGetOrder, values, &order); err != nil {
|
||||
return order, err
|
||||
}
|
||||
|
||||
if !order.Success {
|
||||
return order, errors.New(order.Message)
|
||||
}
|
||||
return order, nil
|
||||
func (b *Bittrex) GetOrder(uuid string) (OrderData, error) {
|
||||
var resp OrderData
|
||||
return resp, b.SendAuthHTTPRequest(exchange.RestSpot, http.MethodGet, fmt.Sprintf(getOrder, uuid), nil, nil, &resp, nil)
|
||||
}
|
||||
|
||||
// GetOrderHistoryForCurrency is used to retrieve your order history. If currencyPair
|
||||
// omitted it will return the entire order History.
|
||||
func (b *Bittrex) GetOrderHistoryForCurrency(currencyPair string) (Order, error) {
|
||||
var orders Order
|
||||
values := url.Values{}
|
||||
|
||||
if !(currencyPair == "" || currencyPair == " ") {
|
||||
values.Set("market", currencyPair)
|
||||
}
|
||||
|
||||
if err := b.SendAuthenticatedHTTPRequest(exchange.RestSpot, "/"+bittrexAPIGetOrderHistory, values, &orders); err != nil {
|
||||
return orders, err
|
||||
}
|
||||
|
||||
if !orders.Success {
|
||||
return orders, errors.New(orders.Message)
|
||||
}
|
||||
return orders, nil
|
||||
// GetOrderHistoryForCurrency is used to retrieve your order history. If marketName
|
||||
// is omitted it will return the entire order History.
|
||||
func (b *Bittrex) GetOrderHistoryForCurrency(currency string) ([]OrderData, error) {
|
||||
var resp []OrderData
|
||||
return resp, b.SendAuthHTTPRequest(exchange.RestSpot, http.MethodGet, fmt.Sprintf(getClosedOrders, currency), nil, nil, &resp, nil)
|
||||
}
|
||||
|
||||
// GetWithdrawalHistory is used to retrieve your withdrawal history. If currency
|
||||
// GetClosedWithdrawals is used to retrieve your withdrawal history.
|
||||
func (b *Bittrex) GetClosedWithdrawals() ([]WithdrawalData, error) {
|
||||
var resp []WithdrawalData
|
||||
|
||||
return resp, b.SendAuthHTTPRequest(exchange.RestSpot, http.MethodGet, getClosedWithdrawals, nil, nil, &resp, nil)
|
||||
}
|
||||
|
||||
// GetClosedWithdrawalsForCurrency is used to retrieve your withdrawal history for the specified currency.
|
||||
func (b *Bittrex) GetClosedWithdrawalsForCurrency(currency string) ([]WithdrawalData, error) {
|
||||
var resp []WithdrawalData
|
||||
|
||||
params := url.Values{}
|
||||
params.Set("currencySymbol", currency)
|
||||
|
||||
return resp, b.SendAuthHTTPRequest(exchange.RestSpot, http.MethodGet, getClosedWithdrawals, params, nil, &resp, nil)
|
||||
}
|
||||
|
||||
// GetOpenWithdrawals is used to retrieve your withdrawal history. If currency
|
||||
// omitted it will return the entire history
|
||||
func (b *Bittrex) GetWithdrawalHistory(currency string) (WithdrawalHistory, error) {
|
||||
var history WithdrawalHistory
|
||||
values := url.Values{}
|
||||
|
||||
if !(currency == "" || currency == " ") {
|
||||
values.Set("currency", currency)
|
||||
}
|
||||
|
||||
if err := b.SendAuthenticatedHTTPRequest(exchange.RestSpot, "/"+bittrexAPIGetWithdrawalHistory, values, &history); err != nil {
|
||||
return history, err
|
||||
}
|
||||
|
||||
if !history.Success {
|
||||
return history, errors.New(history.Message)
|
||||
}
|
||||
return history, nil
|
||||
func (b *Bittrex) GetOpenWithdrawals() ([]WithdrawalData, error) {
|
||||
var resp []WithdrawalData
|
||||
return resp, b.SendAuthHTTPRequest(exchange.RestSpot, http.MethodGet, getOpenWithdrawals, nil, nil, &resp, nil)
|
||||
}
|
||||
|
||||
// GetDepositHistory is used to retrieve your deposit history. If currency is
|
||||
// is omitted it will return the entire deposit history
|
||||
func (b *Bittrex) GetDepositHistory(currency string) (DepositHistory, error) {
|
||||
var history DepositHistory
|
||||
values := url.Values{}
|
||||
// GetClosedDeposits is used to retrieve your deposit history.
|
||||
func (b *Bittrex) GetClosedDeposits() ([]DepositData, error) {
|
||||
var resp []DepositData
|
||||
return resp, b.SendAuthHTTPRequest(exchange.RestSpot, http.MethodGet, getClosedDeposits, nil, nil, &resp, nil)
|
||||
}
|
||||
|
||||
if !(currency == "" || currency == " ") {
|
||||
values.Set("currency", currency)
|
||||
// GetClosedDepositsForCurrency is used to retrieve your deposit history for the specified currency
|
||||
func (b *Bittrex) GetClosedDepositsForCurrency(currency string) ([]DepositData, error) {
|
||||
var resp []DepositData
|
||||
|
||||
params := url.Values{}
|
||||
params.Set("currencySymbol", currency)
|
||||
|
||||
return resp, b.SendAuthHTTPRequest(exchange.RestSpot, http.MethodGet, getClosedDeposits, params, nil, &resp, nil)
|
||||
}
|
||||
|
||||
// GetClosedDepositsPaginated is used to retrieve your deposit history.
|
||||
// The maximum page size is 200 and it defaults to 100.
|
||||
// PreviousPageToken is the unique identifier of the item that the resulting
|
||||
// query result should end before, in the sort order of the given endpoint. Used
|
||||
// for traversing a paginated set in the reverse direction.
|
||||
func (b *Bittrex) GetClosedDepositsPaginated(pageSize int, previousPageTokenOptional ...string) ([]DepositData, error) {
|
||||
var resp []DepositData
|
||||
|
||||
params := url.Values{}
|
||||
params.Set("pageSize", strconv.Itoa(pageSize))
|
||||
|
||||
if len(previousPageTokenOptional) > 0 {
|
||||
params.Set("previousPageToken", previousPageTokenOptional[0])
|
||||
}
|
||||
|
||||
if err := b.SendAuthenticatedHTTPRequest(exchange.RestSpot, "/"+bittrexAPIGetDepositHistory, values, &history); err != nil {
|
||||
return history, err
|
||||
}
|
||||
return resp, b.SendAuthHTTPRequest(exchange.RestSpot, http.MethodGet, getClosedDeposits, params, nil, &resp, nil)
|
||||
}
|
||||
|
||||
if !history.Success {
|
||||
return history, errors.New(history.Message)
|
||||
}
|
||||
return history, nil
|
||||
// GetOpenDeposits is used to retrieve your open deposits.
|
||||
func (b *Bittrex) GetOpenDeposits() ([]DepositData, error) {
|
||||
var resp []DepositData
|
||||
return resp, b.SendAuthHTTPRequest(exchange.RestSpot, http.MethodGet, getOpenDeposits, nil, nil, &resp, nil)
|
||||
}
|
||||
|
||||
// GetOpenDepositsForCurrency is used to retrieve your open deposits for the specified currency
|
||||
func (b *Bittrex) GetOpenDepositsForCurrency(currency string) ([]DepositData, error) {
|
||||
var resp []DepositData
|
||||
|
||||
params := url.Values{}
|
||||
params.Set("currencySymbol", currency)
|
||||
|
||||
return resp, b.SendAuthHTTPRequest(exchange.RestSpot, http.MethodGet, getOpenDeposits, params, nil, &resp, nil)
|
||||
}
|
||||
|
||||
// SendHTTPRequest sends an unauthenticated HTTP request
|
||||
func (b *Bittrex) SendHTTPRequest(ep exchange.URL, path string, result interface{}) error {
|
||||
func (b *Bittrex) SendHTTPRequest(ep exchange.URL, path string, result interface{}, resultHeader *http.Header) error {
|
||||
endpoint, err := b.API.Endpoints.GetURL(ep)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return b.SendPayload(context.Background(), &request.Item{
|
||||
Method: http.MethodGet,
|
||||
Path: endpoint + path,
|
||||
Result: result,
|
||||
Verbose: b.Verbose,
|
||||
HTTPDebugging: b.HTTPDebugging,
|
||||
HTTPRecording: b.HTTPRecording,
|
||||
})
|
||||
requestItem := request.Item{
|
||||
Method: http.MethodGet,
|
||||
Path: endpoint + path,
|
||||
Result: result,
|
||||
Verbose: b.Verbose,
|
||||
HTTPDebugging: b.HTTPDebugging,
|
||||
HTTPRecording: b.HTTPRecording,
|
||||
HeaderResponse: resultHeader,
|
||||
}
|
||||
return b.SendPayload(context.Background(), &requestItem)
|
||||
}
|
||||
|
||||
// SendAuthenticatedHTTPRequest sends an authenticated http request to a desired
|
||||
// path
|
||||
func (b *Bittrex) SendAuthenticatedHTTPRequest(ep exchange.URL, path string, values url.Values, result interface{}) (err error) {
|
||||
// SendAuthHTTPRequest sends an authenticated request
|
||||
func (b *Bittrex) SendAuthHTTPRequest(ep exchange.URL, method, action string, params url.Values, data, result interface{}, resultHeader *http.Header) error {
|
||||
if !b.AllowAuthenticatedRequest() {
|
||||
return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, b.Name)
|
||||
}
|
||||
@@ -427,27 +368,46 @@ func (b *Bittrex) SendAuthenticatedHTTPRequest(ep exchange.URL, path string, val
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
n := b.Requester.GetNonce(true).String()
|
||||
|
||||
values.Set("apikey", b.API.Credentials.Key)
|
||||
values.Set("nonce", n)
|
||||
rawQuery := endpoint + path + "?" + values.Encode()
|
||||
hmac := crypto.GetHMAC(
|
||||
crypto.HashSHA512, []byte(rawQuery), []byte(b.API.Credentials.Secret),
|
||||
)
|
||||
ts := strconv.FormatInt(time.Now().UnixNano()/1000000, 10)
|
||||
|
||||
path := common.EncodeURLValues(action, params)
|
||||
|
||||
var body io.Reader
|
||||
var hmac, payload []byte
|
||||
var contentHash string
|
||||
if data == nil {
|
||||
payload = []byte("")
|
||||
} else {
|
||||
var err error
|
||||
payload, err = json.Marshal(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
body = bytes.NewBuffer(payload)
|
||||
contentHash = crypto.HexEncodeToString(crypto.GetSHA512(payload))
|
||||
sigPayload := ts + endpoint + path + method + contentHash
|
||||
hmac = crypto.GetHMAC(crypto.HashSHA512, []byte(sigPayload), []byte(b.API.Credentials.Secret))
|
||||
|
||||
headers := make(map[string]string)
|
||||
headers["apisign"] = crypto.HexEncodeToString(hmac)
|
||||
|
||||
headers["Api-Key"] = b.API.Credentials.Key
|
||||
headers["Api-Timestamp"] = ts
|
||||
headers["Api-Content-Hash"] = contentHash
|
||||
headers["Api-Signature"] = crypto.HexEncodeToString(hmac)
|
||||
headers["Content-Type"] = "application/json"
|
||||
headers["Accept"] = "application/json"
|
||||
return b.SendPayload(context.Background(), &request.Item{
|
||||
Method: http.MethodGet,
|
||||
Path: rawQuery,
|
||||
Headers: headers,
|
||||
Result: result,
|
||||
AuthRequest: true,
|
||||
NonceEnabled: true,
|
||||
Verbose: b.Verbose,
|
||||
HTTPDebugging: b.HTTPDebugging,
|
||||
HTTPRecording: b.HTTPRecording,
|
||||
Method: method,
|
||||
Path: endpoint + path,
|
||||
Headers: headers,
|
||||
Body: body,
|
||||
Result: result,
|
||||
AuthRequest: true,
|
||||
Verbose: b.Verbose,
|
||||
HTTPDebugging: b.HTTPDebugging,
|
||||
HTTPRecording: b.HTTPRecording,
|
||||
HeaderResponse: resultHeader,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -478,9 +438,9 @@ func (b *Bittrex) GetWithdrawalFee(c currency.Code) (float64, error) {
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
for _, result := range currencies.Result {
|
||||
if result.Currency == c.String() {
|
||||
fee = result.TxFee
|
||||
for i := range currencies {
|
||||
if currencies[i].Symbol == c.String() {
|
||||
fee = currencies[i].TxFee
|
||||
}
|
||||
}
|
||||
return fee, nil
|
||||
@@ -490,7 +450,3 @@ func (b *Bittrex) GetWithdrawalFee(c currency.Code) (float64, error) {
|
||||
func calculateTradingFee(price, amount float64) float64 {
|
||||
return 0.0025 * price * amount
|
||||
}
|
||||
|
||||
func parseTime(t string) (time.Time, error) {
|
||||
return time.Parse(bittrexTimeLayout, t)
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ const (
|
||||
apiKey = ""
|
||||
apiSecret = ""
|
||||
canManipulateRealOrders = false
|
||||
currPair = "USDT-BTC"
|
||||
currPair = "BTC-USDT"
|
||||
curr = "BTC"
|
||||
)
|
||||
|
||||
@@ -98,7 +98,7 @@ func TestGetMarketSummary(t *testing.T) {
|
||||
func TestGetOrderbook(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, err := b.GetOrderbook(currPair)
|
||||
_, _, err := b.GetOrderbook(currPair, 500)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -113,20 +113,35 @@ func TestGetMarketHistory(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlaceBuyLimit(t *testing.T) {
|
||||
func TestGetRecentCandles(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, err := b.PlaceBuyLimit(currPair, 1, 1)
|
||||
if err == nil {
|
||||
t.Error("Expected error")
|
||||
_, err := b.GetRecentCandles(currPair, "HOUR_1", "MIDPOINT")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlaceSellLimit(t *testing.T) {
|
||||
func TestGetHistoricalCandles(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, err := b.PlaceSellLimit(currPair, 1, 1)
|
||||
_, err := b.GetHistoricalCandles(currPair, "MINUTE_5", "MIDPOINT", 2020, 12, 31)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
_, err = b.GetHistoricalCandles(currPair, "MINUTE_5", "MIDPOINT", 2020, 12, 32)
|
||||
if err == nil {
|
||||
t.Error("invalid date should give an error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrder(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, err := b.Order(currPair, order.Buy.String(), order.Limit.String(), "", 1, 1, 0.0)
|
||||
if areTestAPIKeysSet() && err != nil {
|
||||
t.Error(err)
|
||||
} else if !areTestAPIKeysSet() && err == nil {
|
||||
t.Error("Expected error")
|
||||
}
|
||||
}
|
||||
@@ -134,13 +149,13 @@ func TestPlaceSellLimit(t *testing.T) {
|
||||
func TestGetOpenOrders(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, err := b.GetOpenOrders("")
|
||||
_, _, err := b.GetOpenOrders("")
|
||||
if areTestAPIKeysSet() && err != nil {
|
||||
t.Error(err)
|
||||
} else if !areTestAPIKeysSet() && err == nil {
|
||||
t.Error("Expected error")
|
||||
}
|
||||
_, err = b.GetOpenOrders(currPair)
|
||||
_, _, err = b.GetOpenOrders(currPair)
|
||||
if areTestAPIKeysSet() && err != nil {
|
||||
t.Error(err)
|
||||
} else if !areTestAPIKeysSet() && err == nil {
|
||||
@@ -160,7 +175,7 @@ func TestCancelExistingOrder(t *testing.T) {
|
||||
func TestGetAccountBalances(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, err := b.GetAccountBalances()
|
||||
_, err := b.GetBalances()
|
||||
if areTestAPIKeysSet() && err != nil {
|
||||
t.Error(err)
|
||||
} else if !areTestAPIKeysSet() && err == nil {
|
||||
@@ -213,16 +228,10 @@ func TestGetOrderHistoryForCurrency(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetwithdrawalHistory(t *testing.T) {
|
||||
func TestGetClosedWithdrawals(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, err := b.GetWithdrawalHistory("")
|
||||
if areTestAPIKeysSet() && err != nil {
|
||||
t.Error(err)
|
||||
} else if !areTestAPIKeysSet() && err == nil {
|
||||
t.Error("Expected error")
|
||||
}
|
||||
_, err = b.GetWithdrawalHistory(curr)
|
||||
_, err := b.GetClosedWithdrawals()
|
||||
if areTestAPIKeysSet() && err != nil {
|
||||
t.Error(err)
|
||||
} else if !areTestAPIKeysSet() && err == nil {
|
||||
@@ -230,20 +239,94 @@ func TestGetwithdrawalHistory(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetDepositHistory(t *testing.T) {
|
||||
func TestGetClosedWithdrawalsForCurrency(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, err := b.GetDepositHistory("")
|
||||
|
||||
_, err := b.GetClosedWithdrawalsForCurrency(curr)
|
||||
if areTestAPIKeysSet() && err != nil {
|
||||
t.Error(err)
|
||||
} else if !areTestAPIKeysSet() && err == nil {
|
||||
t.Error("Expected error")
|
||||
}
|
||||
_, err = b.GetDepositHistory(currPair)
|
||||
if err == nil {
|
||||
}
|
||||
|
||||
func TestGetOpenWithdrawals(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, err := b.GetOpenWithdrawals()
|
||||
if areTestAPIKeysSet() && err != nil {
|
||||
t.Error(err)
|
||||
} else if !areTestAPIKeysSet() && err == nil {
|
||||
t.Error("Expected error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetClosedDeposits(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, err := b.GetClosedDeposits()
|
||||
if areTestAPIKeysSet() && err != nil {
|
||||
t.Error(err)
|
||||
} else if !areTestAPIKeysSet() && err == nil {
|
||||
t.Error("Expected error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetClosedDepositsForCurrency(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, err := b.GetClosedDepositsForCurrency(curr)
|
||||
if areTestAPIKeysSet() && err != nil {
|
||||
t.Error(err)
|
||||
} else if !areTestAPIKeysSet() && err == nil {
|
||||
t.Error("Expected error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetClosedDepositsPaginated(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, err := b.GetClosedDepositsPaginated(100)
|
||||
if areTestAPIKeysSet() && err != nil {
|
||||
t.Error(err)
|
||||
} else if !areTestAPIKeysSet() && err == nil {
|
||||
t.Error("Expected error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetOpenDeposits(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, err := b.GetOpenDeposits()
|
||||
if areTestAPIKeysSet() && err != nil {
|
||||
t.Error(err)
|
||||
} else if !areTestAPIKeysSet() && err == nil {
|
||||
t.Error("Expected error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetOpenDepositsForCurrency(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, err := b.GetOpenDepositsForCurrency(curr)
|
||||
if areTestAPIKeysSet() && err != nil {
|
||||
t.Error(err)
|
||||
} else if !areTestAPIKeysSet() && err == nil {
|
||||
t.Error("Expected error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithdraw(t *testing.T) {
|
||||
t.Parallel()
|
||||
if !areTestAPIKeysSet() || !canManipulateRealOrders {
|
||||
t.Skip("skipping test, either api keys or canManipulateRealOrders isnt set correctly")
|
||||
}
|
||||
_, err := b.Withdraw(curr, "", core.BitcoinDonationAddress, 0.0009)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func setFeeBuilder() *exchange.FeeBuilder {
|
||||
return &exchange.FeeBuilder{
|
||||
Amount: 1,
|
||||
@@ -356,7 +439,7 @@ func TestGetActiveOrders(t *testing.T) {
|
||||
AssetType: asset.Spot,
|
||||
}
|
||||
|
||||
getOrdersRequest.Pairs[0].Delimiter = "-"
|
||||
getOrdersRequest.Pairs[0].Delimiter = currency.DashDelimiter
|
||||
|
||||
_, err = b.GetActiveOrders(&getOrdersRequest)
|
||||
if areTestAPIKeysSet() && err != nil {
|
||||
@@ -373,6 +456,15 @@ func TestGetOrderHistory(t *testing.T) {
|
||||
}
|
||||
|
||||
_, err := b.GetOrderHistory(&getOrdersRequest)
|
||||
if err == nil {
|
||||
t.Error("Expected: 'At least one currency is required to fetch order history'. received nil")
|
||||
}
|
||||
|
||||
getOrdersRequest.Pairs = []currency.Pair{
|
||||
currency.NewPair(currency.BTC, currency.USDT),
|
||||
}
|
||||
|
||||
_, err = b.GetOrderHistory(&getOrdersRequest)
|
||||
if areTestAPIKeysSet() && err != nil {
|
||||
t.Errorf("Could not get order history: %s", err)
|
||||
} else if !areTestAPIKeysSet() && err == nil {
|
||||
@@ -393,7 +485,7 @@ func TestSubmitOrder(t *testing.T) {
|
||||
|
||||
var orderSubmission = &order.Submit{
|
||||
Pair: currency.Pair{
|
||||
Delimiter: "-",
|
||||
Delimiter: currency.DashDelimiter,
|
||||
Base: currency.BTC,
|
||||
Quote: currency.LTC,
|
||||
},
|
||||
@@ -473,7 +565,7 @@ func TestModifyOrder(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithdraw(t *testing.T) {
|
||||
func WithdrawCryptocurrencyFunds(t *testing.T) {
|
||||
withdrawCryptoRequest := withdraw.Request{
|
||||
Amount: -1,
|
||||
Currency: currency.BTC,
|
||||
@@ -536,24 +628,6 @@ func TestGetDepositAddress(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseTime(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tm, err := parseTime("2019-11-21T02:08:34.87")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if tm.Year() != 2019 ||
|
||||
tm.Month() != 11 ||
|
||||
tm.Day() != 21 ||
|
||||
tm.Hour() != 2 ||
|
||||
tm.Minute() != 8 ||
|
||||
tm.Second() != 34 {
|
||||
t.Error("invalid time values")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRecentTrades(t *testing.T) {
|
||||
t.Parallel()
|
||||
currencyPair, err := currency.NewPairFromString(currPair)
|
||||
|
||||
@@ -1,226 +1,296 @@
|
||||
package bittrex
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/thrasher-corp/gocryptotrader/currency"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/stream"
|
||||
)
|
||||
|
||||
// Response is the generalised response type for Bittrex
|
||||
type Response struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
Result json.RawMessage `json:"result"`
|
||||
// CancelOrderRequest holds request data for CancelOrder
|
||||
type CancelOrderRequest struct {
|
||||
OrderID int64 `json:"orderId,string"`
|
||||
}
|
||||
|
||||
// Market holds current market metadata
|
||||
type Market struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
Result []struct {
|
||||
MarketCurrency string `json:"MarketCurrency"`
|
||||
BaseCurrency string `json:"BaseCurrency"`
|
||||
MarketCurrencyLong string `json:"MarketCurrencyLong"`
|
||||
BaseCurrencyLong string `json:"BaseCurrencyLong"`
|
||||
MinTradeSize float64 `json:"MinTradeSize"`
|
||||
MarketName string `json:"MarketName"`
|
||||
IsActive bool `json:"IsActive"`
|
||||
Created string `json:"Created"`
|
||||
} `json:"result"`
|
||||
// TimeInForce defines timeInForce types
|
||||
type TimeInForce string
|
||||
|
||||
// All order status types
|
||||
const (
|
||||
GoodTilCancelled TimeInForce = "GOOD_TIL_CANCELLED"
|
||||
ImmediateOrCancel TimeInForce = "IMMEDIATE_OR_CANCEL"
|
||||
FillOrKill TimeInForce = "FILL_OR_KILL"
|
||||
PostOnlyGoodTilCancelled TimeInForce = "POST_ONLY_GOOD_TIL_CANCELLED"
|
||||
BuyNow TimeInForce = "BUY_NOW"
|
||||
)
|
||||
|
||||
// OrderData holds order data
|
||||
type OrderData struct {
|
||||
ID string `json:"id"`
|
||||
MarketSymbol string `json:"marketSymbol"`
|
||||
Direction string `json:"direction"`
|
||||
Type string `json:"type"`
|
||||
Quantity float64 `json:"quantity,string"`
|
||||
Limit float64 `json:"limit,string"`
|
||||
Ceiling float64 `json:"ceiling,string"`
|
||||
TimeInForce string `json:"timeInForce"`
|
||||
ClientOrderID string `json:"clientOrderId"`
|
||||
FillQuantity float64 `json:"fillQuantity,string"`
|
||||
Commission float64 `json:"commission,string"`
|
||||
Proceeds float64 `json:"proceeds,string"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
ClosedAt time.Time `json:"closedAt"`
|
||||
OrderToCancel struct {
|
||||
Type string `json:"type,string"`
|
||||
ID string `json:"id,string"`
|
||||
} `json:"orderToCancel"`
|
||||
}
|
||||
|
||||
// Currency holds supported currency metadata
|
||||
type Currency struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
Result []struct {
|
||||
Currency string `json:"Currency"`
|
||||
CurrencyLong string `json:"CurrencyLong"`
|
||||
MinConfirmation int64 `json:"MinConfirmation"`
|
||||
TxFee float64 `json:"TxFee"`
|
||||
IsActive bool `json:"IsActive"`
|
||||
CoinType string `json:"CoinType"`
|
||||
BaseAddress string `json:"BaseAddress"`
|
||||
} `json:"result"`
|
||||
// BulkCancelResultData holds the result of a bulk cancel action
|
||||
type BulkCancelResultData struct {
|
||||
ID string `json:"id"`
|
||||
StatusCode string `json:"statusCode"`
|
||||
Result OrderData `json:"result"`
|
||||
}
|
||||
|
||||
// Ticker holds basic ticker information
|
||||
type Ticker struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
Result struct {
|
||||
Bid float64 `json:"Bid"`
|
||||
Ask float64 `json:"Ask"`
|
||||
Last float64 `json:"Last"`
|
||||
} `json:"result"`
|
||||
// MarketData stores market data
|
||||
type MarketData struct {
|
||||
Symbol string `json:"symbol"`
|
||||
BaseCurrencySymbol string `json:"baseCurrencySymbol"`
|
||||
QuoteCurrencySymbol string `json:"quoteCurrencySymbol"`
|
||||
MinTradeSize float64 `json:"minTradeSize,string"`
|
||||
Precision int32 `json:"precision"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
Notice string `json:"notice"`
|
||||
ProhibitedIn []string `json:"prohibitedIn"`
|
||||
}
|
||||
|
||||
// MarketSummary holds last 24 hour metadata of an active exchange
|
||||
type MarketSummary struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
Result []struct {
|
||||
MarketName string `json:"MarketName"`
|
||||
High float64 `json:"High"`
|
||||
Low float64 `json:"Low"`
|
||||
Volume float64 `json:"Volume"`
|
||||
Last float64 `json:"Last"`
|
||||
BaseVolume float64 `json:"BaseVolume"`
|
||||
TimeStamp string `json:"TimeStamp"`
|
||||
Bid float64 `json:"Bid"`
|
||||
Ask float64 `json:"Ask"`
|
||||
OpenBuyOrders int64 `json:"OpenBuyOrders"`
|
||||
OpenSellOrders int64 `json:"OpenSellOrders"`
|
||||
PrevDay float64 `json:"PrevDay"`
|
||||
Created string `json:"Created"`
|
||||
DisplayMarketName string `json:"DisplayMarketName"`
|
||||
} `json:"result"`
|
||||
// TickerData stores ticker data
|
||||
type TickerData struct {
|
||||
Symbol string `json:"symbol"`
|
||||
LastTradeRate float64 `json:"lastTradeRate,string"`
|
||||
BidRate float64 `json:"bidRate,string"`
|
||||
AskRate float64 `json:"askRate,string"`
|
||||
}
|
||||
|
||||
// OrderBooks holds an array of buy & sell orders held on the exchange
|
||||
type OrderBooks struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
Result struct {
|
||||
Buy []OrderBook `json:"buy"`
|
||||
Sell []OrderBook `json:"sell"`
|
||||
} `json:"result"`
|
||||
// TradeData stores trades data
|
||||
type TradeData struct {
|
||||
ID string `json:"id"`
|
||||
ExecutedAt time.Time `json:"executedAt"`
|
||||
Quantity float64 `json:"quantity,string"`
|
||||
Rate float64 `json:"rate,string"`
|
||||
TakerSide string `json:"takerSide"`
|
||||
}
|
||||
|
||||
// OrderBook holds a singular order on an exchange
|
||||
type OrderBook struct {
|
||||
Quantity float64 `json:"Quantity"`
|
||||
Rate float64 `json:"Rate"`
|
||||
// MarketSummaryData stores market summary data
|
||||
type MarketSummaryData struct {
|
||||
Symbol string `json:"symbol"`
|
||||
High float64 `json:"high,string"`
|
||||
Low float64 `json:"low,string"`
|
||||
Volume float64 `json:"volume,string"`
|
||||
QuoteVolume float64 `json:"quoteVolume,string"`
|
||||
PercentChange float64 `json:"percentChange,string"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
// MarketHistory holds an executed trade's data for a market ie "BTC-LTC"
|
||||
type MarketHistory struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
Result []struct {
|
||||
ID int64 `json:"Id"`
|
||||
Timestamp string `json:"TimeStamp"`
|
||||
Quantity float64 `json:"Quantity"`
|
||||
Price float64 `json:"Price"`
|
||||
Total float64 `json:"Total"`
|
||||
FillType string `json:"FillType"`
|
||||
OrderType string `json:"OrderType"`
|
||||
} `json:"result"`
|
||||
// OrderbookData holds the order book data
|
||||
type OrderbookData struct {
|
||||
Bid []OrderbookEntryData `json:"bid"`
|
||||
Ask []OrderbookEntryData `json:"ask"`
|
||||
}
|
||||
|
||||
// Balance holds the balance from your account for a specified currency
|
||||
type Balance struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
Result struct {
|
||||
Currency string `json:"Currency"`
|
||||
Balance float64 `json:"Balance"`
|
||||
Available float64 `json:"Available"`
|
||||
Pending float64 `json:"Pending"`
|
||||
CryptoAddress string `json:"CryptoAddress"`
|
||||
Requested bool `json:"Requested"`
|
||||
UUID string `json:"Uuid"`
|
||||
} `json:"result"`
|
||||
// OrderbookEntryData holds an order book entry
|
||||
type OrderbookEntryData struct {
|
||||
Quantity float64 `json:"quantity,string"`
|
||||
Rate float64 `json:"rate,string"`
|
||||
}
|
||||
|
||||
// Balances holds the balance from your account for a specified currency
|
||||
type Balances struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
Result []struct {
|
||||
Currency string `json:"Currency"`
|
||||
Balance float64 `json:"Balance"`
|
||||
Available float64 `json:"Available"`
|
||||
Pending float64 `json:"Pending"`
|
||||
CryptoAddress string `json:"CryptoAddress"`
|
||||
Requested bool `json:"Requested"`
|
||||
UUID string `json:"Uuid"`
|
||||
} `json:"result"`
|
||||
// BalanceData holds balance data
|
||||
type BalanceData struct {
|
||||
CurrencySymbol string `json:"currencySymbol"`
|
||||
Total float64 `json:"total,string"`
|
||||
Available float64 `json:"available,string"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
// DepositAddress holds a generated address to send specific coins to the
|
||||
// exchange
|
||||
type DepositAddress struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
Result struct {
|
||||
Currency string `json:"Currency"`
|
||||
Address string `json:"Address"`
|
||||
} `json:"result"`
|
||||
// AddressData holds address data
|
||||
// Status is REQUESTED or PROVISIONED
|
||||
type AddressData struct {
|
||||
Status string `json:"status"`
|
||||
CurrencySymbol string `json:"currencySymbol"`
|
||||
CryptoAddress string `json:"cryptoAddress"`
|
||||
CryptoAddressTag string `json:"cryptoAddressTag"`
|
||||
}
|
||||
|
||||
// UUID contains the universal unique identifier for one or multiple
|
||||
// transactions on the exchange
|
||||
type UUID struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
Result struct {
|
||||
ID string `json:"uuid"`
|
||||
} `json:"result"`
|
||||
// CurrencyData holds currency data
|
||||
// Status is ONLINE or OFFLINE
|
||||
type CurrencyData struct {
|
||||
Symbol string `json:"symbol"`
|
||||
Name string `json:"name"`
|
||||
CoinType string `json:"coinType"`
|
||||
Status string `json:"status"`
|
||||
MinConfirmations int32 `json:"minConfirmations"`
|
||||
Notice string `json:"notice"`
|
||||
TxFee float64 `json:"txFee,string"`
|
||||
LogoURL string `json:"logoUrl"`
|
||||
ProhibitedIn []string `json:"prohibitedIn"`
|
||||
}
|
||||
|
||||
// Order holds the full order information associated with the UUID supplied
|
||||
type Order struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
Result []struct {
|
||||
AccountID string `json:"AccountId"`
|
||||
OrderUUID string `json:"OrderUuid"`
|
||||
Exchange string `json:"Exchange"`
|
||||
Type string `json:"Type"`
|
||||
Quantity float64 `json:"Quantity"`
|
||||
QuantityRemaining float64 `json:"QuantityRemaining"`
|
||||
Limit float64 `json:"Limit"`
|
||||
Reserved float64 `json:"Reserved"`
|
||||
ReserveRemaining float64 `json:"ReserveRemaining"`
|
||||
CommissionReserved float64 `json:"CommissionReserved"`
|
||||
CommissionReserveRemaining float64 `json:"CommissionReserveRemaining"`
|
||||
CommissionPaid float64 `json:"CommissionPaid"`
|
||||
Price float64 `json:"Price"`
|
||||
PricePerUnit float64 `json:"PricePerUnit"`
|
||||
Opened string `json:"Opened"`
|
||||
Closed string `json:"Closed"`
|
||||
IsOpen bool `json:"IsOpen"`
|
||||
Sentinel string `json:"Sentinel"`
|
||||
CancelInitiated bool `json:"CancelInitiated"`
|
||||
ImmediateOrCancel bool `json:"ImmediateOrCancel"`
|
||||
IsConditional bool `json:"IsConditional"`
|
||||
Condition string `json:"Condition"`
|
||||
ConditionTarget string `json:"ConditionTarget"`
|
||||
// Below Used in OrderHistory
|
||||
TimeStamp string `json:"TimeStamp"`
|
||||
Commission float64 `json:"Commission"`
|
||||
} `json:"result"`
|
||||
// WithdrawalData holds withdrawal data
|
||||
type WithdrawalData struct {
|
||||
ID string `json:"id"`
|
||||
CurrencySymbol string `json:"currencySymbol"`
|
||||
Quantity float64 `json:"quantity,string"`
|
||||
CryptoAddress string `json:"cryptoAddress"`
|
||||
CryptoAddressTag string `json:"cryptoAddressTag"`
|
||||
TxCost float64 `json:"txCost,string"`
|
||||
TxID string `json:"txId"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
CompletedAt time.Time `json:"completedAt"`
|
||||
ClientWithdrawalID string `json:"clientWithdrawalId"`
|
||||
}
|
||||
|
||||
// WithdrawalHistory holds the Withdrawal history data
|
||||
type WithdrawalHistory struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
Result []struct {
|
||||
PaymentUUID string `json:"PaymentUuid"`
|
||||
Currency string `json:"Currency"`
|
||||
Amount float64 `json:"Amount"`
|
||||
Address string `json:"Address"`
|
||||
Opened string `json:"Opened"`
|
||||
Authorized bool `json:"Authorized"`
|
||||
PendingPayment bool `json:"PendingPayment"`
|
||||
TxCost float64 `json:"TxCost"`
|
||||
TxID string `json:"TxId"`
|
||||
Canceled bool `json:"Canceled"`
|
||||
InvalidAddress bool `json:"InvalidAddress"`
|
||||
} `json:"result"`
|
||||
// DepositData holds deposit data
|
||||
type DepositData struct {
|
||||
ID string `json:"id"`
|
||||
CurrencySymbol string `json:"currencySymbol"`
|
||||
Quantity float64 `json:"quantity,string"`
|
||||
CryptoAddress string `json:"cryptoAddress"`
|
||||
CryptoAddressTag string `json:"cryptoAddressTag"`
|
||||
TxID string `json:"txId"`
|
||||
Confirmations int32 `json:"confirmations"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
CompletedAt time.Time `json:"completedAt"`
|
||||
Status string `json:"status"`
|
||||
Source string `json:"source"`
|
||||
}
|
||||
|
||||
// DepositHistory holds the Deposit history data
|
||||
type DepositHistory struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
Result []struct {
|
||||
ID int64 `json:"Id"`
|
||||
Amount float64 `json:"Amount"`
|
||||
Currency string `json:"Currency"`
|
||||
Confirmations int64 `json:"Confirmations"`
|
||||
LastUpdated string `json:"LastUpdated"`
|
||||
TxID string `json:"TxId"`
|
||||
CryptoAddress string `json:"CryptoAddress"`
|
||||
} `json:"result"`
|
||||
// CandleData holds candle data
|
||||
type CandleData struct {
|
||||
StartsAt time.Time `json:"startsAt"`
|
||||
Open float64 `json:"open,string"`
|
||||
High float64 `json:"high,string"`
|
||||
Low float64 `json:"low,string"`
|
||||
Close float64 `json:"close,string"`
|
||||
Volume float64 `json:"volume,string"`
|
||||
QuoteVolume float64 `json:"quoteVolume,string"`
|
||||
}
|
||||
|
||||
// WsSignalRHandshakeData holds data for the SignalR websocket wrapper handshake
|
||||
type WsSignalRHandshakeData struct {
|
||||
URL string `json:"Url"` // Path to the SignalR endpoint
|
||||
ConnectionToken string `json:"ConnectionToken"` // Connection token assigned by the server
|
||||
ConnectionID string `json:"ConnectionId"` // The ID of the connection
|
||||
KeepAliveTimeout float64 `json:"KeepAliveTimeout"` // Representing the amount of time to wait before sending a keep alive packet over an idle connection
|
||||
DisconnectTimeout float64 `json:"DisconnectTimeout"` // Represents the amount of time to wait after a connection goes away before raising the disconnect event
|
||||
ConnectionTimeout float64 `json:"ConnectionTimeout"` // Represents the amount of time to leave a connection open before timing out
|
||||
TryWebSockets bool `json:"TryWebSockets"` // Whether the server supports websockets
|
||||
ProtocolVersion string `json:"ProtocolVersion"` // The version of the protocol used for communication
|
||||
TransportConnectTimeout float64 `json:"TransportConnectTimeout"` // The maximum amount of time the client should try to connect to the server using a given transport
|
||||
LongPollDelay float64 `json:"LongPollDelay"` // The time to tell the browser to wait before reestablishing a long poll connection after data is sent from the server.
|
||||
}
|
||||
|
||||
// WsEventRequest holds data on websocket requests
|
||||
type WsEventRequest struct {
|
||||
Hub string `json:"H"`
|
||||
Method string `json:"M"`
|
||||
Arguments interface{} `json:"A"`
|
||||
InvocationID int64 `json:"I"`
|
||||
}
|
||||
|
||||
// WsEventStatus holds data on the websocket event status
|
||||
type WsEventStatus struct {
|
||||
Success bool `json:"Success"`
|
||||
ErrorCode string `json:"ErrorCode"`
|
||||
}
|
||||
|
||||
// WsEventResponse holds data on the websocket response
|
||||
type WsEventResponse struct {
|
||||
C string `json:"C"`
|
||||
S int `json:"S"`
|
||||
G string `json:"G"`
|
||||
Response interface{} `json:"R"`
|
||||
InvocationID int64 `json:"I,string"`
|
||||
Message []struct {
|
||||
Hub string `json:"H"`
|
||||
Method string `json:"M"`
|
||||
Arguments []string `json:"A"`
|
||||
} `json:"M"`
|
||||
}
|
||||
|
||||
// WsSubscriptionResponse holds data on the websocket response
|
||||
type WsSubscriptionResponse struct {
|
||||
C string `json:"C"`
|
||||
S int `json:"S"`
|
||||
G string `json:"G"`
|
||||
Response []WsEventStatus `json:"R"`
|
||||
InvocationID int64 `json:"I,string"`
|
||||
Message []struct {
|
||||
Hub string `json:"H"`
|
||||
Method string `json:"M"`
|
||||
Arguments []string `json:"A"`
|
||||
} `json:"M"`
|
||||
}
|
||||
|
||||
// WsAuthResponse holds data on the websocket response
|
||||
type WsAuthResponse struct {
|
||||
C string `json:"C"`
|
||||
S int `json:"S"`
|
||||
G string `json:"G"`
|
||||
Response WsEventStatus `json:"R"`
|
||||
InvocationID int64 `json:"I,string"`
|
||||
Message []struct {
|
||||
Hub string `json:"H"`
|
||||
Method string `json:"M"`
|
||||
Arguments []string `json:"A"`
|
||||
} `json:"M"`
|
||||
}
|
||||
|
||||
// OrderbookUpdateMessage holds websocket orderbook update messages
|
||||
type OrderbookUpdateMessage struct {
|
||||
MarketSymbol string `json:"marketSymbol"`
|
||||
Depth int `json:"depth"`
|
||||
Sequence int64 `json:"sequence"`
|
||||
BidDeltas []OrderbookEntryData `json:"bidDeltas"`
|
||||
AskDeltas []OrderbookEntryData `json:"askDeltas"`
|
||||
}
|
||||
|
||||
// OrderUpdateMessage holds websocket order update messages
|
||||
type OrderUpdateMessage struct {
|
||||
AccountID string `json:"accountId"`
|
||||
Sequence int `json:"int,string"`
|
||||
Delta OrderData `json:"delta"`
|
||||
}
|
||||
|
||||
// WsPendingRequest holds pending requests
|
||||
type WsPendingRequest struct {
|
||||
WsEventRequest
|
||||
ChannelsToSubscribe *[]stream.ChannelSubscription
|
||||
}
|
||||
|
||||
// orderbookManager defines a way of managing and maintaining synchronisation
|
||||
// across connections and assets.
|
||||
type orderbookManager struct {
|
||||
state map[currency.Code]map[currency.Code]map[asset.Item]*update
|
||||
sync.Mutex
|
||||
|
||||
jobs chan job
|
||||
}
|
||||
|
||||
type update struct {
|
||||
buffer chan *OrderbookUpdateMessage
|
||||
fetchingBook bool
|
||||
initialSync bool
|
||||
}
|
||||
|
||||
// job defines a synchonisation job that tells a go routine to fetch an
|
||||
// orderbook via the REST protocol
|
||||
type job struct {
|
||||
Pair currency.Pair
|
||||
}
|
||||
|
||||
616
exchanges/bittrex/bittrex_websocket.go
Normal file
616
exchanges/bittrex/bittrex_websocket.go
Normal file
@@ -0,0 +1,616 @@
|
||||
package bittrex
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/flate"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/thrasher-corp/gocryptotrader/common"
|
||||
"github.com/thrasher-corp/gocryptotrader/common/crypto"
|
||||
"github.com/thrasher-corp/gocryptotrader/currency"
|
||||
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/stream"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
|
||||
"github.com/thrasher-corp/gocryptotrader/log"
|
||||
)
|
||||
|
||||
const (
|
||||
bittrexAPIWSURL = "wss://socket-v3.bittrex.com/signalr"
|
||||
bittrexAPIWSNegotiationsURL = "https://socket-v3.bittrex.com/signalr"
|
||||
|
||||
bittrexWebsocketTimer = 13 * time.Second
|
||||
wsTicker = "ticker"
|
||||
wsOrderbook = "orderbook"
|
||||
wsMarketSummary = "market_summary"
|
||||
wsOrders = "order"
|
||||
wsHeartbeat = "heartbeat"
|
||||
authenticate = "Authenticate"
|
||||
subscribe = "subscribe"
|
||||
unsubscribe = "unsubscribe"
|
||||
wsRateLimit = 50
|
||||
wsMessageRateLimit = 60
|
||||
)
|
||||
|
||||
var defaultSpotSubscribedChannels = []string{
|
||||
// wsHeartbeat,
|
||||
wsOrderbook,
|
||||
wsTicker,
|
||||
wsMarketSummary,
|
||||
}
|
||||
|
||||
var defaultSpotSubscribedChannelsAuth = []string{
|
||||
wsOrders,
|
||||
}
|
||||
|
||||
type TickerCache struct {
|
||||
MarketSummaries map[string]*MarketSummaryData
|
||||
Tickers map[string]*TickerData
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
// WsConnect connects to a websocket feed
|
||||
func (b *Bittrex) WsConnect() error {
|
||||
if !b.Websocket.IsEnabled() || !b.IsEnabled() {
|
||||
return errors.New(stream.WebsocketNotEnabled)
|
||||
}
|
||||
|
||||
var wsHandshakeData WsSignalRHandshakeData
|
||||
err := b.WsSignalRHandshake(&wsHandshakeData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var dialer websocket.Dialer
|
||||
endpoint, err := b.API.Endpoints.GetURL(exchange.WebsocketSpot)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
params := url.Values{}
|
||||
params.Set("clientProtocol", "1.5")
|
||||
params.Set("transport", "webSockets")
|
||||
params.Set("connectionToken", wsHandshakeData.ConnectionToken)
|
||||
params.Set("connectionData", "[{name:\"c3\"}]")
|
||||
params.Set("tid", "10")
|
||||
|
||||
path := common.EncodeURLValues("/connect", params)
|
||||
|
||||
err = b.Websocket.SetWebsocketURL(endpoint+path, false, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = b.Websocket.Conn.Dial(&dialer, http.Header{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Can set up custom ping handler per websocket connection.
|
||||
b.Websocket.Conn.SetupPingHandler(stream.PingHandler{
|
||||
MessageType: websocket.PingMessage,
|
||||
Delay: bittrexWebsocketTimer,
|
||||
})
|
||||
|
||||
// This reader routine is called prior to initiating a subscription for
|
||||
// efficient processing.
|
||||
go b.wsReadData()
|
||||
b.setupOrderbookManager()
|
||||
b.tickerCache = &TickerCache{
|
||||
MarketSummaries: make(map[string]*MarketSummaryData),
|
||||
Tickers: make(map[string]*TickerData),
|
||||
}
|
||||
|
||||
if b.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
|
||||
err = b.WsAuth()
|
||||
if err != nil {
|
||||
b.Websocket.DataHandler <- err
|
||||
b.Websocket.SetCanUseAuthenticatedEndpoints(false)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WsSignalRHandshake requests the SignalR connection token over https
|
||||
func (b *Bittrex) WsSignalRHandshake(result interface{}) error {
|
||||
endpoint, err := b.API.Endpoints.GetURL(exchange.WebsocketSpotSupplementary)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
path := "/negotiate?connectionData=[{name:\"c3\"}]&clientProtocol=1.5"
|
||||
return b.SendPayload(context.Background(), &request.Item{
|
||||
Method: http.MethodGet,
|
||||
Path: endpoint + path,
|
||||
Result: result,
|
||||
Verbose: b.Verbose,
|
||||
HTTPDebugging: b.HTTPDebugging,
|
||||
HTTPRecording: b.HTTPRecording,
|
||||
})
|
||||
}
|
||||
|
||||
// WsAuth sends an authentication message to receive auth data
|
||||
// Authentications expire after 10 minutes
|
||||
func (b *Bittrex) WsAuth() error {
|
||||
// [apiKey, timestamp in ms, random uuid, signed payload]
|
||||
apiKey := b.API.Credentials.Key
|
||||
randomContent, err := uuid.NewV4()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
timestamp := strconv.FormatInt(time.Now().UnixNano()/1000000, 10)
|
||||
hmac := crypto.GetHMAC(
|
||||
crypto.HashSHA512,
|
||||
[]byte(timestamp+randomContent.String()),
|
||||
[]byte(b.API.Credentials.Secret),
|
||||
)
|
||||
signature := crypto.HexEncodeToString(hmac)
|
||||
|
||||
req := WsEventRequest{
|
||||
Hub: "c3",
|
||||
Method: authenticate,
|
||||
InvocationID: b.Websocket.Conn.GenerateMessageID(false),
|
||||
}
|
||||
|
||||
arguments := make([]string, 0)
|
||||
arguments = append(arguments, apiKey, timestamp, randomContent.String(), signature)
|
||||
req.Arguments = arguments
|
||||
|
||||
requestString, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if b.Verbose {
|
||||
log.Debugf(log.WebsocketMgr, "%s Sending JSON message - %s\n", b.Name, requestString)
|
||||
}
|
||||
|
||||
respRaw, err := b.Websocket.Conn.SendMessageReturnResponse(req.InvocationID, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var response WsAuthResponse
|
||||
err = json.Unmarshal(respRaw, &response)
|
||||
if err != nil {
|
||||
log.Warnf(log.WebsocketMgr, "%s - Cannot unmarshal into WsAuthResponse (%s)\n", b.Name, string(respRaw))
|
||||
return err
|
||||
}
|
||||
if !response.Response.Success {
|
||||
log.Warnf(log.WebsocketMgr, "%s - Unable to authenticate (%s)", b.Name, response.Response.ErrorCode)
|
||||
b.Websocket.SetCanUseAuthenticatedEndpoints(false)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be
|
||||
// handled by ManageSubscriptions()
|
||||
func (b *Bittrex) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, error) {
|
||||
var subscriptions []stream.ChannelSubscription
|
||||
pairs, err := b.GetEnabledPairs(asset.Spot)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
channels := defaultSpotSubscribedChannels
|
||||
if b.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
|
||||
channels = append(channels, defaultSpotSubscribedChannelsAuth...)
|
||||
}
|
||||
|
||||
for i := range pairs {
|
||||
pair, err := b.FormatExchangeCurrency(pairs[i], asset.Spot)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for y := range channels {
|
||||
var channel string
|
||||
switch channels[y] {
|
||||
case wsOrderbook:
|
||||
channel = channels[y] + "_" + pair.String() + "_" + strconv.FormatInt(orderbookDepth, 10)
|
||||
case wsTicker:
|
||||
channel = channels[y] + "_" + pair.String()
|
||||
case wsMarketSummary:
|
||||
channel = channels[y] + "_" + pair.String()
|
||||
default:
|
||||
channel = channels[y]
|
||||
}
|
||||
subscriptions = append(subscriptions,
|
||||
stream.ChannelSubscription{
|
||||
Channel: channel,
|
||||
Currency: pair,
|
||||
Asset: asset.Spot,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return subscriptions, nil
|
||||
}
|
||||
|
||||
// Subscribe sends a websocket message to receive data from the channel
|
||||
func (b *Bittrex) Subscribe(channelsToSubscribe []stream.ChannelSubscription) error {
|
||||
var x int
|
||||
var errs common.Errors
|
||||
for x = 0; x+wsMessageRateLimit < len(channelsToSubscribe); x += wsMessageRateLimit {
|
||||
err := b.subscribeSlice(channelsToSubscribe[x : x+wsMessageRateLimit])
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
err := b.subscribeSlice(channelsToSubscribe[x:])
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
if errs != nil {
|
||||
return errs
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Bittrex) subscribeSlice(channelsToSubscribe []stream.ChannelSubscription) error {
|
||||
req := WsEventRequest{
|
||||
Hub: "c3",
|
||||
Method: subscribe,
|
||||
InvocationID: b.Websocket.Conn.GenerateMessageID(false),
|
||||
}
|
||||
|
||||
var channels []string
|
||||
for i := range channelsToSubscribe {
|
||||
channels = append(channels, channelsToSubscribe[i].Channel)
|
||||
}
|
||||
arguments := make([][]string, 0)
|
||||
arguments = append(arguments, channels)
|
||||
req.Arguments = arguments
|
||||
|
||||
requestString, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if b.Verbose {
|
||||
log.Debugf(log.WebsocketMgr, "%s - Sending JSON message - %s\n", b.Name, requestString)
|
||||
}
|
||||
respRaw, err := b.Websocket.Conn.SendMessageReturnResponse(req.InvocationID, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var response WsSubscriptionResponse
|
||||
err = json.Unmarshal(respRaw, &response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var errs common.Errors
|
||||
for i := range response.Response {
|
||||
if !response.Response[i].Success {
|
||||
errs = append(errs, errors.New("unable to subscribe to "+channels[i]+" - error code "+response.Response[i].ErrorCode))
|
||||
continue
|
||||
}
|
||||
b.Websocket.AddSuccessfulSubscriptions(channelsToSubscribe[i])
|
||||
}
|
||||
if errs != nil {
|
||||
return errs
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Unsubscribe sends a websocket message to receive data from the channel
|
||||
func (b *Bittrex) Unsubscribe(channelsToUnsubscribe []stream.ChannelSubscription) error {
|
||||
var x int
|
||||
var errs common.Errors
|
||||
for x = 0; x+wsMessageRateLimit < len(channelsToUnsubscribe); x += wsMessageRateLimit {
|
||||
err := b.unsubscribeSlice(channelsToUnsubscribe[x : x+wsMessageRateLimit])
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
err := b.unsubscribeSlice(channelsToUnsubscribe[x:])
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
if errs != nil {
|
||||
return errs
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Bittrex) unsubscribeSlice(channelsToUnsubscribe []stream.ChannelSubscription) error {
|
||||
req := WsEventRequest{
|
||||
Hub: "c3",
|
||||
Method: unsubscribe,
|
||||
InvocationID: b.Websocket.Conn.GenerateMessageID(false),
|
||||
}
|
||||
|
||||
var channels []string
|
||||
for i := range channelsToUnsubscribe {
|
||||
channels = append(channels, channelsToUnsubscribe[i].Channel)
|
||||
}
|
||||
arguments := make([][]string, 0)
|
||||
arguments = append(arguments, channels)
|
||||
req.Arguments = arguments
|
||||
|
||||
requestString, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if b.Verbose {
|
||||
log.Debugf(log.WebsocketMgr, "%s - Sending JSON message - %s\n", b.Name, requestString)
|
||||
}
|
||||
respRaw, err := b.Websocket.Conn.SendMessageReturnResponse(req.InvocationID, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var response WsSubscriptionResponse
|
||||
err = json.Unmarshal(respRaw, &response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var errs common.Errors
|
||||
for i := range response.Response {
|
||||
if !response.Response[i].Success {
|
||||
errs = append(errs, errors.New("unable to unsubscribe from "+channels[i]+" - error code "+response.Response[i].ErrorCode))
|
||||
continue
|
||||
}
|
||||
b.Websocket.RemoveSuccessfulUnsubscriptions(channelsToUnsubscribe[i])
|
||||
}
|
||||
if errs != nil {
|
||||
return errs
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// wsReadData gets and passes on websocket messages for processing
|
||||
func (b *Bittrex) wsReadData() {
|
||||
b.Websocket.Wg.Add(1)
|
||||
defer b.Websocket.Wg.Done()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-b.Websocket.ShutdownC:
|
||||
return
|
||||
default:
|
||||
resp := b.Websocket.Conn.ReadMessage()
|
||||
if resp.Raw == nil {
|
||||
log.Warnf(log.WebsocketMgr, "%s Received empty message\n", b.Name)
|
||||
return
|
||||
}
|
||||
|
||||
err := b.wsHandleData(resp.Raw)
|
||||
if err != nil {
|
||||
b.Websocket.DataHandler <- err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bittrex) wsDecodeMessage(encodedMessage string, v interface{}) error {
|
||||
raw, err := crypto.Base64Decode(encodedMessage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reader := flate.NewReader(bytes.NewBuffer(raw))
|
||||
message, err := ioutil.ReadAll(reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(message, v)
|
||||
}
|
||||
|
||||
func (b *Bittrex) wsHandleData(respRaw []byte) error {
|
||||
var response WsEventResponse
|
||||
err := json.Unmarshal(respRaw, &response)
|
||||
if err != nil {
|
||||
log.Warnf(log.WebsocketMgr, "%s Cannot unmarshal into eventResponse (%s)\n", b.Name, string(respRaw))
|
||||
return err
|
||||
}
|
||||
if response.Response != nil && response.InvocationID > 0 {
|
||||
if b.Websocket.Match.IncomingWithData(response.InvocationID, respRaw) {
|
||||
return nil
|
||||
}
|
||||
return errors.New("received response to unknown request")
|
||||
}
|
||||
|
||||
if response.Response == nil && len(response.Message) == 0 && response.C == "" {
|
||||
if b.Verbose {
|
||||
log.Warnf(log.WebsocketMgr, "%s Received keep-alive (%s)\n", b.Name, string(respRaw))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
for i := range response.Message {
|
||||
switch response.Message[i].Method {
|
||||
case "orderBook":
|
||||
for j := range response.Message[i].Arguments {
|
||||
var orderbookUpdate OrderbookUpdateMessage
|
||||
err = b.wsDecodeMessage(response.Message[i].Arguments[j], &orderbookUpdate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var init bool
|
||||
init, err = b.UpdateLocalOBBuffer(&orderbookUpdate)
|
||||
if err != nil {
|
||||
if init {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("%v - UpdateLocalCache error: %s",
|
||||
b.Name,
|
||||
err)
|
||||
}
|
||||
}
|
||||
case "ticker":
|
||||
for j := range response.Message[i].Arguments {
|
||||
var tickerUpdate TickerData
|
||||
err = b.wsDecodeMessage(response.Message[i].Arguments[j], &tickerUpdate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = b.WsProcessUpdateTicker(tickerUpdate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case "marketSummary":
|
||||
for j := range response.Message[i].Arguments {
|
||||
var marketSummaryUpdate MarketSummaryData
|
||||
err = b.wsDecodeMessage(response.Message[i].Arguments[j], &marketSummaryUpdate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = b.WsProcessUpdateMarketSummary(&marketSummaryUpdate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case "heartbeat":
|
||||
if b.Verbose {
|
||||
log.Warnf(log.WebsocketMgr, "%s Received heartbeat\n", b.Name)
|
||||
}
|
||||
case "authenticationExpiring":
|
||||
if b.Verbose {
|
||||
log.Debugf(log.WebsocketMgr, "%s - Re-authenticating.\n", b.Name)
|
||||
}
|
||||
err = b.WsAuth()
|
||||
if err != nil {
|
||||
b.Websocket.DataHandler <- err
|
||||
b.Websocket.SetCanUseAuthenticatedEndpoints(false)
|
||||
}
|
||||
case "order":
|
||||
for j := range response.Message[i].Arguments {
|
||||
var orderUpdate OrderUpdateMessage
|
||||
err = b.wsDecodeMessage(response.Message[i].Arguments[j], &orderUpdate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = b.WsProcessUpdateOrder(&orderUpdate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WsProcessUpdateTicker processes an update on the ticker
|
||||
func (b *Bittrex) WsProcessUpdateTicker(tickerData TickerData) error {
|
||||
pair, err := currency.NewPairFromString(tickerData.Symbol)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tickerPrice, err := ticker.GetTicker(b.Name, pair, asset.Spot)
|
||||
if err != nil {
|
||||
b.tickerCache.Lock()
|
||||
defer b.tickerCache.Unlock()
|
||||
if b.tickerCache.MarketSummaries[tickerData.Symbol] != nil {
|
||||
marketSummaryData := b.tickerCache.MarketSummaries[tickerData.Symbol]
|
||||
tickerPrice = b.constructTicker(tickerData, marketSummaryData, pair, asset.Spot)
|
||||
b.Websocket.DataHandler <- tickerPrice
|
||||
return nil
|
||||
}
|
||||
b.tickerCache.Tickers[tickerData.Symbol] = &tickerData
|
||||
return nil
|
||||
}
|
||||
|
||||
tickerPrice.Last = tickerData.LastTradeRate
|
||||
tickerPrice.Bid = tickerData.BidRate
|
||||
tickerPrice.Ask = tickerData.AskRate
|
||||
|
||||
b.Websocket.DataHandler <- tickerPrice
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WsProcessUpdateMarketSummary processes an update on the ticker
|
||||
func (b *Bittrex) WsProcessUpdateMarketSummary(marketSummaryData *MarketSummaryData) error {
|
||||
pair, err := currency.NewPairFromString(marketSummaryData.Symbol)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tickerPrice, err := ticker.GetTicker(b.Name, pair, asset.Spot)
|
||||
if err != nil {
|
||||
b.tickerCache.Lock()
|
||||
defer b.tickerCache.Unlock()
|
||||
if b.tickerCache.Tickers[marketSummaryData.Symbol] != nil {
|
||||
tickerData := b.tickerCache.Tickers[marketSummaryData.Symbol]
|
||||
tickerPrice = b.constructTicker(*tickerData, marketSummaryData, pair, asset.Spot)
|
||||
b.Websocket.DataHandler <- tickerPrice
|
||||
return nil
|
||||
}
|
||||
b.tickerCache.MarketSummaries[marketSummaryData.Symbol] = marketSummaryData
|
||||
return nil
|
||||
}
|
||||
|
||||
tickerPrice.High = marketSummaryData.High
|
||||
tickerPrice.Low = marketSummaryData.Low
|
||||
tickerPrice.Volume = marketSummaryData.Volume
|
||||
tickerPrice.QuoteVolume = marketSummaryData.QuoteVolume
|
||||
tickerPrice.LastUpdated = marketSummaryData.UpdatedAt
|
||||
|
||||
b.Websocket.DataHandler <- tickerPrice
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WsProcessUpdateOrder processes an update on the open orders
|
||||
func (b *Bittrex) WsProcessUpdateOrder(data *OrderUpdateMessage) error {
|
||||
orderType, err := order.StringToOrderType(data.Delta.Type)
|
||||
if err != nil {
|
||||
b.Websocket.DataHandler <- order.ClassificationError{
|
||||
Exchange: b.Name,
|
||||
OrderID: data.Delta.ID,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
orderSide, err := order.StringToOrderSide(data.Delta.Direction)
|
||||
if err != nil {
|
||||
b.Websocket.DataHandler <- order.ClassificationError{
|
||||
Exchange: b.Name,
|
||||
OrderID: data.Delta.ID,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
orderStatus, err := order.StringToOrderStatus(data.Delta.Status)
|
||||
if err != nil {
|
||||
b.Websocket.DataHandler <- order.ClassificationError{
|
||||
Exchange: b.Name,
|
||||
OrderID: data.Delta.ID,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
pair, err := currency.NewPairFromString(data.Delta.MarketSymbol)
|
||||
if err != nil {
|
||||
b.Websocket.DataHandler <- order.ClassificationError{
|
||||
Exchange: b.Name,
|
||||
OrderID: data.Delta.ID,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
b.Websocket.DataHandler <- &order.Modify{
|
||||
ImmediateOrCancel: data.Delta.TimeInForce == string(ImmediateOrCancel),
|
||||
FillOrKill: data.Delta.TimeInForce == string(GoodTilCancelled),
|
||||
PostOnly: data.Delta.TimeInForce == string(PostOnlyGoodTilCancelled),
|
||||
Price: data.Delta.Limit,
|
||||
Amount: data.Delta.Quantity,
|
||||
RemainingAmount: data.Delta.Quantity - data.Delta.FillQuantity,
|
||||
ExecutedAmount: data.Delta.FillQuantity,
|
||||
Exchange: b.Name,
|
||||
ID: data.Delta.ID,
|
||||
Type: orderType,
|
||||
Side: orderSide,
|
||||
Status: orderStatus,
|
||||
AssetType: asset.Spot,
|
||||
Date: data.Delta.CreatedAt,
|
||||
Pair: pair,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
441
exchanges/bittrex/bittrex_ws_orderbook.go
Normal file
441
exchanges/bittrex/bittrex_ws_orderbook.go
Normal file
@@ -0,0 +1,441 @@
|
||||
package bittrex
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/thrasher-corp/gocryptotrader/currency"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/stream/buffer"
|
||||
"github.com/thrasher-corp/gocryptotrader/log"
|
||||
)
|
||||
|
||||
var (
|
||||
// maxWSUpdateBuffer defines max websocket updates to apply when an
|
||||
// orderbook is initially fetched
|
||||
maxWSUpdateBuffer = 150
|
||||
// maxWSOrderbookJobs defines max websocket orderbook jobs in queue to fetch
|
||||
// an orderbook snapshot via REST
|
||||
maxWSOrderbookJobs = 2000
|
||||
// maxWSOrderbookWorkers defines a max amount of workers allowed to execute
|
||||
// jobs from the job channel
|
||||
maxWSOrderbookWorkers = 10
|
||||
)
|
||||
|
||||
func (b *Bittrex) setupOrderbookManager() {
|
||||
if b.obm == nil {
|
||||
b.obm = &orderbookManager{
|
||||
state: make(map[currency.Code]map[currency.Code]map[asset.Item]*update),
|
||||
jobs: make(chan job, maxWSOrderbookJobs),
|
||||
}
|
||||
|
||||
for i := 0; i < maxWSOrderbookWorkers; i++ {
|
||||
// 10 workers for synchronising book
|
||||
b.SynchroniseWebsocketOrderbook()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ProcessUpdateOB processes the websocket orderbook update
|
||||
func (b *Bittrex) ProcessUpdateOB(pair currency.Pair, message *OrderbookUpdateMessage) error {
|
||||
var updateBids []orderbook.Item
|
||||
for x := range message.BidDeltas {
|
||||
updateBids = append(updateBids, orderbook.Item{
|
||||
Price: message.BidDeltas[x].Rate,
|
||||
Amount: message.BidDeltas[x].Quantity,
|
||||
})
|
||||
}
|
||||
var updateAsks []orderbook.Item
|
||||
for x := range message.AskDeltas {
|
||||
updateAsks = append(updateAsks, orderbook.Item{
|
||||
Price: message.AskDeltas[x].Rate,
|
||||
Amount: message.AskDeltas[x].Quantity,
|
||||
})
|
||||
}
|
||||
|
||||
return b.Websocket.Orderbook.Update(&buffer.Update{
|
||||
Asset: asset.Spot,
|
||||
Pair: pair,
|
||||
UpdateID: message.Sequence,
|
||||
MaxDepth: orderbookDepth,
|
||||
Bids: updateBids,
|
||||
Asks: updateAsks,
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateLocalBuffer updates and returns the most recent iteration of the orderbook
|
||||
func (b *Bittrex) UpdateLocalOBBuffer(update *OrderbookUpdateMessage) (bool, error) {
|
||||
enabledPairs, err := b.GetEnabledPairs(asset.Spot)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
format, err := b.GetPairFormat(asset.Spot, true)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
currencyPair, err := currency.NewPairFromFormattedPairs(update.MarketSymbol,
|
||||
enabledPairs,
|
||||
format)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
err = b.obm.stageWsUpdate(update, currencyPair, asset.Spot)
|
||||
if err != nil {
|
||||
init, err2 := b.obm.checkIsInitialSync(currencyPair)
|
||||
if err2 != nil {
|
||||
return false, err2
|
||||
}
|
||||
return init, err
|
||||
}
|
||||
|
||||
err = b.applyBufferUpdate(currencyPair)
|
||||
if err != nil {
|
||||
b.flushAndCleanup(currencyPair)
|
||||
}
|
||||
|
||||
return false, err
|
||||
}
|
||||
|
||||
// SeedLocalOBCache seeds depth data
|
||||
func (b *Bittrex) SeedLocalOBCache(p currency.Pair) error {
|
||||
ob, sequence, err := b.GetOrderbook(p.String(), orderbookDepth)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return b.SeedLocalCacheWithOrderBook(p, sequence, &ob)
|
||||
}
|
||||
|
||||
// SeedLocalCacheWithOrderBook seeds the local orderbook cache
|
||||
func (b *Bittrex) SeedLocalCacheWithOrderBook(p currency.Pair, sequence int64, orderbookNew *OrderbookData) error {
|
||||
var newOrderBook orderbook.Base
|
||||
for i := range orderbookNew.Bid {
|
||||
newOrderBook.Bids = append(newOrderBook.Bids, orderbook.Item{
|
||||
Amount: orderbookNew.Bid[i].Quantity,
|
||||
Price: orderbookNew.Bid[i].Rate,
|
||||
})
|
||||
}
|
||||
for i := range orderbookNew.Ask {
|
||||
newOrderBook.Asks = append(newOrderBook.Asks, orderbook.Item{
|
||||
Amount: orderbookNew.Ask[i].Quantity,
|
||||
Price: orderbookNew.Ask[i].Rate,
|
||||
})
|
||||
}
|
||||
|
||||
newOrderBook.Pair = p
|
||||
newOrderBook.Asset = asset.Spot
|
||||
newOrderBook.Exchange = b.Name
|
||||
newOrderBook.LastUpdateID = sequence
|
||||
newOrderBook.VerifyOrderbook = b.CanVerifyOrderbook
|
||||
|
||||
return b.Websocket.Orderbook.LoadSnapshot(&newOrderBook)
|
||||
}
|
||||
|
||||
// applyBufferUpdate applies the buffer to the orderbook or initiates a new
|
||||
// orderbook sync by the REST protocol which is off handed to go routine.
|
||||
func (b *Bittrex) applyBufferUpdate(pair currency.Pair) error {
|
||||
fetching, err := b.obm.checkIsFetchingBook(pair)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if fetching {
|
||||
return nil
|
||||
}
|
||||
|
||||
recent, err := b.Websocket.Orderbook.GetOrderbook(pair, asset.Spot)
|
||||
if err != nil || (recent.Asks == nil && recent.Bids == nil) {
|
||||
if b.Verbose {
|
||||
log.Debugf(log.WebsocketMgr, "Orderbook: Fetching via REST\n")
|
||||
}
|
||||
return b.obm.fetchBookViaREST(pair)
|
||||
}
|
||||
|
||||
return b.obm.checkAndProcessUpdate(b.ProcessUpdateOB, pair, recent)
|
||||
}
|
||||
|
||||
// SynchroniseWebsocketOrderbook synchronises full orderbook for currency pair
|
||||
// asset
|
||||
func (b *Bittrex) SynchroniseWebsocketOrderbook() {
|
||||
b.Websocket.Wg.Add(1)
|
||||
go func() {
|
||||
defer b.Websocket.Wg.Done()
|
||||
for {
|
||||
select {
|
||||
case j := <-b.obm.jobs:
|
||||
err := b.processJob(j.Pair)
|
||||
if err != nil {
|
||||
log.Errorf(log.WebsocketMgr,
|
||||
"%s processing websocket orderbook error %v",
|
||||
b.Name, err)
|
||||
}
|
||||
case <-b.Websocket.ShutdownC:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// processJob fetches and processes orderbook updates
|
||||
func (b *Bittrex) processJob(p currency.Pair) error {
|
||||
err := b.SeedLocalOBCache(p)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s %s seeding local cache for orderbook error: %v",
|
||||
p, asset.Spot, err)
|
||||
}
|
||||
|
||||
err = b.obm.stopFetchingBook(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Immediately apply the buffer updates so we don't wait for a
|
||||
// new update to initiate this.
|
||||
err = b.applyBufferUpdate(p)
|
||||
if err != nil {
|
||||
b.flushAndCleanup(p)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// flushAndCleanup flushes orderbook and clean local cache
|
||||
func (b *Bittrex) flushAndCleanup(p currency.Pair) {
|
||||
errClean := b.Websocket.Orderbook.FlushOrderbook(p, asset.Spot)
|
||||
if errClean != nil {
|
||||
log.Errorf(log.WebsocketMgr,
|
||||
"%s flushing websocket error: %v",
|
||||
b.Name,
|
||||
errClean)
|
||||
}
|
||||
errClean = b.obm.cleanup(p)
|
||||
if errClean != nil {
|
||||
log.Errorf(log.WebsocketMgr, "%s cleanup websocket error: %v",
|
||||
b.Name,
|
||||
errClean)
|
||||
}
|
||||
}
|
||||
|
||||
// stageWsUpdate stages websocket update to roll through updates that need to
|
||||
// be applied to a fetched orderbook via REST.
|
||||
func (o *orderbookManager) stageWsUpdate(u *OrderbookUpdateMessage, pair currency.Pair, a asset.Item) error {
|
||||
o.Lock()
|
||||
defer o.Unlock()
|
||||
m1, ok := o.state[pair.Base]
|
||||
if !ok {
|
||||
m1 = make(map[currency.Code]map[asset.Item]*update)
|
||||
o.state[pair.Base] = m1
|
||||
}
|
||||
|
||||
m2, ok := m1[pair.Quote]
|
||||
if !ok {
|
||||
m2 = make(map[asset.Item]*update)
|
||||
m1[pair.Quote] = m2
|
||||
}
|
||||
|
||||
state, ok := m2[a]
|
||||
if !ok {
|
||||
state = &update{
|
||||
// 100ms update assuming we might have up to a 10 second delay.
|
||||
// There could be a potential 100 updates for the currency.
|
||||
buffer: make(chan *OrderbookUpdateMessage, maxWSUpdateBuffer),
|
||||
fetchingBook: false,
|
||||
initialSync: true,
|
||||
}
|
||||
m2[a] = state
|
||||
}
|
||||
|
||||
select {
|
||||
// Put update in the channel buffer to be processed
|
||||
case state.buffer <- u:
|
||||
return nil
|
||||
default:
|
||||
<-state.buffer // pop one element
|
||||
state.buffer <- u // to shift buffer on fail
|
||||
return fmt.Errorf("channel blockage for %s, asset %s and connection",
|
||||
pair, a)
|
||||
}
|
||||
}
|
||||
|
||||
// checkIsFetchingBook checks status if the book is currently being via the REST
|
||||
// protocol.
|
||||
func (o *orderbookManager) checkIsFetchingBook(pair currency.Pair) (bool, error) {
|
||||
o.Lock()
|
||||
defer o.Unlock()
|
||||
state, ok := o.state[pair.Base][pair.Quote][asset.Spot]
|
||||
if !ok {
|
||||
return false,
|
||||
fmt.Errorf("check is fetching book cannot match currency pair %s asset type %s",
|
||||
pair,
|
||||
asset.Spot)
|
||||
}
|
||||
return state.fetchingBook, nil
|
||||
}
|
||||
|
||||
// stopFetchingBook completes the book fetching.
|
||||
func (o *orderbookManager) stopFetchingBook(pair currency.Pair) error {
|
||||
o.Lock()
|
||||
defer o.Unlock()
|
||||
state, ok := o.state[pair.Base][pair.Quote][asset.Spot]
|
||||
if !ok {
|
||||
return fmt.Errorf("could not match pair %s and asset type %s in hash table",
|
||||
pair,
|
||||
asset.Spot)
|
||||
}
|
||||
if !state.fetchingBook {
|
||||
return fmt.Errorf("fetching book already set to false for %s %s",
|
||||
pair,
|
||||
asset.Spot)
|
||||
}
|
||||
state.fetchingBook = false
|
||||
return nil
|
||||
}
|
||||
|
||||
// completeInitialSync sets if an asset type has completed its initial sync
|
||||
func (o *orderbookManager) completeInitialSync(pair currency.Pair) error {
|
||||
o.Lock()
|
||||
defer o.Unlock()
|
||||
state, ok := o.state[pair.Base][pair.Quote][asset.Spot]
|
||||
if !ok {
|
||||
return fmt.Errorf("complete initial sync cannot match currency pair %s asset type %s",
|
||||
pair,
|
||||
asset.Spot)
|
||||
}
|
||||
if !state.initialSync {
|
||||
return fmt.Errorf("initital sync already set to false for %s %s",
|
||||
pair,
|
||||
asset.Spot)
|
||||
}
|
||||
state.initialSync = false
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkIsInitialSync checks status if the book is Initial Sync being via the REST
|
||||
// protocol.
|
||||
func (o *orderbookManager) checkIsInitialSync(pair currency.Pair) (bool, error) {
|
||||
o.Lock()
|
||||
defer o.Unlock()
|
||||
state, ok := o.state[pair.Base][pair.Quote][asset.Spot]
|
||||
if !ok {
|
||||
return false,
|
||||
fmt.Errorf("checkIsInitialSync of orderbook cannot match currency pair %s asset type %s",
|
||||
pair,
|
||||
asset.Spot)
|
||||
}
|
||||
return state.initialSync, nil
|
||||
}
|
||||
|
||||
// fetchBookViaREST pushes a job of fetching the orderbook via the REST protocol
|
||||
// to get an initial full book that we can apply our buffered updates too.
|
||||
func (o *orderbookManager) fetchBookViaREST(pair currency.Pair) error {
|
||||
o.Lock()
|
||||
defer o.Unlock()
|
||||
|
||||
state, ok := o.state[pair.Base][pair.Quote][asset.Spot]
|
||||
if !ok {
|
||||
return fmt.Errorf("fetch book via rest cannot match currency pair %s asset type %s",
|
||||
pair,
|
||||
asset.Spot)
|
||||
}
|
||||
|
||||
state.initialSync = true
|
||||
state.fetchingBook = true
|
||||
|
||||
select {
|
||||
case o.jobs <- job{pair}:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("%s %s book synchronisation channel blocked up",
|
||||
pair,
|
||||
asset.Spot)
|
||||
}
|
||||
}
|
||||
|
||||
func (o *orderbookManager) checkAndProcessUpdate(processor func(currency.Pair, *OrderbookUpdateMessage) error, pair currency.Pair, recent *orderbook.Base) error {
|
||||
o.Lock()
|
||||
defer o.Unlock()
|
||||
state, ok := o.state[pair.Base][pair.Quote][asset.Spot]
|
||||
if !ok {
|
||||
return fmt.Errorf("could not match pair [%s] asset type [%s] in hash table to process websocket orderbook update",
|
||||
pair, asset.Spot)
|
||||
}
|
||||
|
||||
// This will continuously remove updates from the buffered channel and
|
||||
// apply them to the current orderbook.
|
||||
buffer:
|
||||
for {
|
||||
select {
|
||||
case d := <-state.buffer:
|
||||
process, err := state.validate(d, recent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if process {
|
||||
err := processor(pair, d)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s %s processing update error: %w",
|
||||
pair, asset.Spot, err)
|
||||
}
|
||||
recent.LastUpdateID = d.Sequence
|
||||
}
|
||||
default:
|
||||
break buffer
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validate checks for correct update alignment
|
||||
func (u *update) validate(updt *OrderbookUpdateMessage, recent *orderbook.Base) (bool, error) {
|
||||
if updt.Sequence <= recent.LastUpdateID {
|
||||
// Drop any event where u is <= lastUpdateId in the snapshot.
|
||||
return false, nil
|
||||
}
|
||||
|
||||
id := recent.LastUpdateID + 1
|
||||
if u.initialSync {
|
||||
// The first processed event should have U <= lastUpdateId+1 AND
|
||||
// u >= lastUpdateId+1.
|
||||
if updt.Sequence > id {
|
||||
return false, fmt.Errorf("initial websocket orderbook sync failure for pair %s and asset %s",
|
||||
recent.Pair,
|
||||
asset.Spot)
|
||||
}
|
||||
u.initialSync = false
|
||||
} else if updt.Sequence != id {
|
||||
// While listening to the stream, each new event's U should be
|
||||
// equal to the previous event's u+1.
|
||||
return false, fmt.Errorf("websocket orderbook synchronisation failure for pair %s and asset %s",
|
||||
recent.Pair,
|
||||
asset.Spot)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// cleanup cleans up buffer and reset fetch and init
|
||||
func (o *orderbookManager) cleanup(pair currency.Pair) error {
|
||||
o.Lock()
|
||||
state, ok := o.state[pair.Base][pair.Quote][asset.Spot]
|
||||
if !ok {
|
||||
o.Unlock()
|
||||
return fmt.Errorf("cleanup cannot match %s %s to hash table",
|
||||
pair,
|
||||
asset.Spot)
|
||||
}
|
||||
|
||||
bufferEmpty:
|
||||
for {
|
||||
select {
|
||||
case <-state.buffer:
|
||||
// bleed and discard buffer
|
||||
default:
|
||||
break bufferEmpty
|
||||
}
|
||||
}
|
||||
o.Unlock()
|
||||
// disable rest orderbook synchronisation
|
||||
_ = o.stopFetchingBook(pair)
|
||||
_ = o.completeInitialSync(pair)
|
||||
return nil
|
||||
}
|
||||
@@ -66,7 +66,7 @@ _b in this context is an `IBotExchange` implemented struct_
|
||||
| Bithumb | Yes | NA | No |
|
||||
| BitMEX | Yes | Yes | Yes |
|
||||
| Bitstamp | Yes | Yes | No |
|
||||
| Bittrex | Yes | No | No |
|
||||
| Bittrex | Yes | Yes | No |
|
||||
| BTCMarkets | Yes | Yes | No |
|
||||
| BTSE | Yes | Yes | No |
|
||||
| Coinbene | Yes | Yes | No |
|
||||
|
||||
4
testdata/configtest.json
vendored
4
testdata/configtest.json
vendored
@@ -770,12 +770,12 @@
|
||||
"tickerBatching": true,
|
||||
"autoPairUpdates": true
|
||||
},
|
||||
"websocketAPI": false,
|
||||
"websocketAPI": true,
|
||||
"websocketCapabilities": {}
|
||||
},
|
||||
"enabled": {
|
||||
"autoPairUpdates": true,
|
||||
"websocketAPI": false
|
||||
"websocketAPI": true
|
||||
}
|
||||
},
|
||||
"bankAccounts": [
|
||||
|
||||
Reference in New Issue
Block a user