mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-06-05 07:26:47 +00:00
* Websocket: Use ErrSubscribedAlready instead of errChannelAlreadySubscribed * Subscriptions: Replace Pair with Pairs Given that some subscriptions have multiple pairs, support that as the standard. * Docs: Update subscriptions in add new exch * RPC: Update Subscription Pairs * Linter: Disable testifylint.Len We deliberately use Equal over Len to avoid spamming the contents of large Slices * Websocket: Add suffix to state consts * Binance: Subscription Pairs support * Bitfinex: Subscription Pairs support * Bithumb: Subscription Pairs support * Bitmex: Subscription Pairs support * Bitstamp: Subscription Pairs support * BTCMarkets: Subscription Pairs support * BTSE: Subscription Pairs support * Coinbase: Subscription Pairs support * Coinut: Subscription Pairs support * GateIO: Subscription Pairs support * Gemini: Subscription Pairs support and improvement * Hitbtc: Subscription Pairs support * Huboi: Subscription Pairs support * Kucoin: Subscription Pairs support * Okcoin: Subscription Pairs support * Poloniex: Subscription Pairs support * Kraken: Add subscription Pairs support Note: This is a naieve implementation because we want to rebase the kraken websocket rewrite on top of this * Bybit: Subscription Pairs support * Okx: Subscription Pairs support * Bitmex: Subsription configuration * Fixes unauthenticated websocket left as CanUseAuth * Fixes auth subs happening privately * CoinbasePro: Subscription Configuration * Consolidate ProductIDs when all subscriptions are for the same list * Websocket: Log actual sent message when Verbose * Subscriptions: Improve clarity of which key is which in Match * Subscriptions: Lint fix for HugeParam * Subscriptions: Add AddPairs and move keys from test * Subscriptions: Simplify subscription keys and add key types * Subscriptions: Add List.GroupPairs Rename sub.AddPairs * Subscription: Fix ExactKey not matching 0 pairs * Subscriptions: Remove unused IdentityKey and HasPairKey * Subscriptions: Fix GetKey test * Subscriptions: Test coverage improvements * Websocket: Change State on Add/Remove * Subscriptions: Improve error context * Subscriptions: Fix Enable: false subs not ignored * Bitfinex: Fix WsAuth test failing on DataHandler DataHandler is eaten by dataMonitor now, so we need to use ToRoutine * Deribit: Subscription Pairs support * Websocket: Accept nil lists for checkSubscriptions If the user passes in a nil (implicitly empty) list, we would not panic. Therefore the burden of correctness about that data lies with them. The list of subscriptions is empty, and that's okay, and possibly convenient * Websocket: Add context to NilPointer errors * Subscriptions: Add context to nil errors * Exchange: Fix error expectations in UnsubToWSChans
933 lines
27 KiB
Go
933 lines
27 KiB
Go
package btcmarkets
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"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/order"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
|
|
)
|
|
|
|
var (
|
|
errInvalidAmount = errors.New("cannot be less than or equal to zero")
|
|
errIDRequired = errors.New("id is required")
|
|
)
|
|
|
|
const (
|
|
btcMarketsAPIURL = "https://api.btcmarkets.net"
|
|
tradeBaseURL = "https://app.btcmarkets.net/buy-sell?market="
|
|
btcMarketsAPIVersion = "/v3"
|
|
|
|
// UnAuthenticated EPs
|
|
btcMarketsAllMarkets = "/markets"
|
|
btcMarketsGetTicker = "/ticker"
|
|
btcMarketsGetTrades = "/trades?"
|
|
btcMarketOrderBook = "/orderbook?"
|
|
btcMarketsCandles = "/candles?"
|
|
btcMarketsTickers = "tickers?"
|
|
btcMarketsMultipleOrderbooks = "orderbooks?"
|
|
btcMarketsGetTime = "/time"
|
|
btcMarketsWithdrawalFees = "/withdrawal-fees"
|
|
btcMarketsUnauthPath = btcMarketsAPIURL + btcMarketsAPIVersion + btcMarketsAllMarkets
|
|
|
|
// Authenticated EPs
|
|
btcMarketsAccountBalance = "/accounts/me/balances"
|
|
btcMarketsTradingFees = "/accounts/me/trading-fees"
|
|
btcMarketsTransactions = "/accounts/me/transactions"
|
|
btcMarketsOrders = "/orders"
|
|
btcMarketsTradeHistory = "/trades"
|
|
btcMarketsWithdrawals = "/withdrawals"
|
|
btcMarketsDeposits = "/deposits"
|
|
btcMarketsTransfers = "/transfers"
|
|
btcMarketsAddresses = "/addresses"
|
|
btcMarketsAssets = "/assets"
|
|
btcMarketsReports = "/reports"
|
|
btcMarketsBatchOrders = "/batchorders"
|
|
|
|
orderFailed = "Failed"
|
|
orderPartiallyCancelled = "Partially Cancelled"
|
|
orderCancelled = "Cancelled"
|
|
orderFullyMatched = "Fully Matched"
|
|
orderPartiallyMatched = "Partially Matched"
|
|
orderPlaced = "Placed"
|
|
orderAccepted = "Accepted"
|
|
|
|
ask = "ask"
|
|
|
|
// order types
|
|
limit = "Limit"
|
|
market = "Market"
|
|
stopLimit = "Stop Limit"
|
|
stop = "Stop"
|
|
takeProfit = "Take Profit"
|
|
|
|
// order sides
|
|
askSide = "Ask"
|
|
bidSide = "Bid"
|
|
|
|
// time in force
|
|
immediateOrCancel = "IOC"
|
|
fillOrKill = "FOK"
|
|
|
|
subscribe = "subscribe"
|
|
fundChange = "fundChange"
|
|
orderChange = "orderChange"
|
|
heartbeat = "heartbeat"
|
|
tick = "tick"
|
|
wsOrderbookUpdate = "orderbookUpdate"
|
|
tradeEndPoint = "trade"
|
|
|
|
// Subscription management when connection and subscription established
|
|
addSubscription = "addSubscription"
|
|
removeSubscription = "removeSubscription"
|
|
clientType = "api"
|
|
)
|
|
|
|
// BTCMarkets is the overarching type across the BTCMarkets package
|
|
type BTCMarkets struct {
|
|
exchange.Base
|
|
}
|
|
|
|
// GetMarkets returns the BTCMarkets instruments
|
|
func (b *BTCMarkets) GetMarkets(ctx context.Context) ([]Market, error) {
|
|
var resp []Market
|
|
return resp, b.SendHTTPRequest(ctx, btcMarketsUnauthPath, &resp)
|
|
}
|
|
|
|
// GetTicker returns a ticker
|
|
// symbol - example "btc" or "ltc"
|
|
func (b *BTCMarkets) GetTicker(ctx context.Context, marketID string) (Ticker, error) {
|
|
var tick Ticker
|
|
return tick, b.SendHTTPRequest(ctx, btcMarketsUnauthPath+"/"+marketID+btcMarketsGetTicker, &tick)
|
|
}
|
|
|
|
// GetTrades returns executed trades on the exchange
|
|
func (b *BTCMarkets) GetTrades(ctx context.Context, marketID string, before, after, limit int64) ([]Trade, error) {
|
|
if (before > 0) && (after >= 0) {
|
|
return nil, errors.New("BTCMarkets only supports either before or after, not both")
|
|
}
|
|
var trades []Trade
|
|
params := url.Values{}
|
|
if before > 0 {
|
|
params.Set("before", strconv.FormatInt(before, 10))
|
|
}
|
|
if after > 0 {
|
|
params.Set("after", strconv.FormatInt(after, 10))
|
|
}
|
|
if limit > 0 {
|
|
params.Set("limit", strconv.FormatInt(limit, 10))
|
|
}
|
|
return trades, b.SendHTTPRequest(ctx, btcMarketsUnauthPath+"/"+marketID+btcMarketsGetTrades+params.Encode(),
|
|
&trades)
|
|
}
|
|
|
|
// GetOrderbook returns current orderbook.
|
|
// levels are:
|
|
// 0 - Returns the top bids and ask orders only.
|
|
// 1 - Returns top 50 bids and asks.
|
|
// 2 - Returns full orderbook. WARNING: This is cached every 10 seconds.
|
|
func (b *BTCMarkets) GetOrderbook(ctx context.Context, marketID string, level int64) (*Orderbook, error) {
|
|
params := url.Values{}
|
|
if level != 0 {
|
|
params.Set("level", strconv.FormatInt(level, 10))
|
|
}
|
|
var temp tempOrderbook
|
|
err := b.SendHTTPRequest(ctx, btcMarketsUnauthPath+"/"+marketID+btcMarketOrderBook+params.Encode(),
|
|
&temp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
orderbook := Orderbook{
|
|
MarketID: temp.MarketID,
|
|
SnapshotID: temp.SnapshotID,
|
|
Bids: make([]OBData, len(temp.Bids)),
|
|
Asks: make([]OBData, len(temp.Asks)),
|
|
}
|
|
|
|
for x := range temp.Asks {
|
|
price, err := strconv.ParseFloat(temp.Asks[x][0], 64)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
amount, err := strconv.ParseFloat(temp.Asks[x][1], 64)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
orderbook.Asks[x] = OBData{
|
|
Price: price,
|
|
Volume: amount,
|
|
}
|
|
}
|
|
for a := range temp.Bids {
|
|
price, err := strconv.ParseFloat(temp.Bids[a][0], 64)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
amount, err := strconv.ParseFloat(temp.Bids[a][1], 64)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
orderbook.Bids[a] = OBData{
|
|
Price: price,
|
|
Volume: amount,
|
|
}
|
|
}
|
|
return &orderbook, nil
|
|
}
|
|
|
|
// GetMarketCandles gets candles for specified currency pair
|
|
func (b *BTCMarkets) GetMarketCandles(ctx context.Context, marketID, timeWindow string, from, to time.Time, before, after, limit int64) (out CandleResponse, err error) {
|
|
if (before > 0) && (after >= 0) {
|
|
return out, errors.New("BTCMarkets only supports either before or after, not both")
|
|
}
|
|
params := url.Values{}
|
|
params.Set("timeWindow", timeWindow)
|
|
|
|
if from.After(to) && !to.IsZero() {
|
|
return out, errors.New("start time cannot be after end time")
|
|
}
|
|
if !from.IsZero() {
|
|
params.Set("from", from.UTC().Format(time.RFC3339))
|
|
}
|
|
if !to.IsZero() {
|
|
params.Set("to", to.UTC().Format(time.RFC3339))
|
|
}
|
|
if before > 0 {
|
|
params.Set("before", strconv.FormatInt(before, 10))
|
|
}
|
|
if after >= 0 {
|
|
params.Set("after", strconv.FormatInt(after, 10))
|
|
}
|
|
if limit > 0 {
|
|
params.Set("limit", strconv.FormatInt(limit, 10))
|
|
}
|
|
return out, b.SendHTTPRequest(ctx, btcMarketsUnauthPath+"/"+marketID+btcMarketsCandles+params.Encode(), &out)
|
|
}
|
|
|
|
// GetTickers gets multiple tickers
|
|
func (b *BTCMarkets) GetTickers(ctx context.Context, marketIDs currency.Pairs) ([]Ticker, error) {
|
|
var tickers []Ticker
|
|
params := url.Values{}
|
|
for x := range marketIDs {
|
|
params.Add("marketId", marketIDs[x].String())
|
|
}
|
|
return tickers, b.SendHTTPRequest(ctx, btcMarketsUnauthPath+"/"+btcMarketsTickers+params.Encode(),
|
|
&tickers)
|
|
}
|
|
|
|
// GetMultipleOrderbooks gets orderbooks
|
|
func (b *BTCMarkets) GetMultipleOrderbooks(ctx context.Context, marketIDs []string) ([]Orderbook, error) {
|
|
var temp []tempOrderbook
|
|
params := url.Values{}
|
|
for x := range marketIDs {
|
|
params.Add("marketId", marketIDs[x])
|
|
}
|
|
err := b.SendHTTPRequest(ctx, btcMarketsUnauthPath+"/"+btcMarketsMultipleOrderbooks+params.Encode(),
|
|
&temp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
orderbooks := make([]Orderbook, 0, len(marketIDs))
|
|
for i := range temp {
|
|
var tempOB Orderbook
|
|
var price, volume float64
|
|
tempOB.MarketID = temp[i].MarketID
|
|
tempOB.SnapshotID = temp[i].SnapshotID
|
|
tempOB.Asks = make([]OBData, len(temp[i].Asks))
|
|
tempOB.Bids = make([]OBData, len(temp[i].Bids))
|
|
for a := range temp[i].Asks {
|
|
volume, err = strconv.ParseFloat(temp[i].Asks[a][1], 64)
|
|
if err != nil {
|
|
return orderbooks, err
|
|
}
|
|
price, err = strconv.ParseFloat(temp[i].Asks[a][0], 64)
|
|
if err != nil {
|
|
return orderbooks, err
|
|
}
|
|
tempOB.Asks[a] = OBData{Price: price, Volume: volume}
|
|
}
|
|
for y := range temp[i].Bids {
|
|
volume, err = strconv.ParseFloat(temp[i].Bids[y][1], 64)
|
|
if err != nil {
|
|
return orderbooks, err
|
|
}
|
|
price, err = strconv.ParseFloat(temp[i].Bids[y][0], 64)
|
|
if err != nil {
|
|
return orderbooks, err
|
|
}
|
|
tempOB.Bids[y] = OBData{Price: price, Volume: volume}
|
|
}
|
|
orderbooks = append(orderbooks, tempOB)
|
|
}
|
|
return orderbooks, nil
|
|
}
|
|
|
|
// GetCurrentServerTime gets time from btcmarkets
|
|
func (b *BTCMarkets) GetCurrentServerTime(ctx context.Context) (time.Time, error) {
|
|
var resp TimeResp
|
|
return resp.Time, b.SendHTTPRequest(ctx, btcMarketsAPIURL+btcMarketsAPIVersion+btcMarketsGetTime,
|
|
&resp)
|
|
}
|
|
|
|
// GetAccountBalance returns the full account balance
|
|
func (b *BTCMarkets) GetAccountBalance(ctx context.Context) ([]AccountData, error) {
|
|
var resp []AccountData
|
|
return resp,
|
|
b.SendAuthenticatedRequest(ctx, http.MethodGet,
|
|
btcMarketsAccountBalance,
|
|
nil,
|
|
&resp,
|
|
request.Auth)
|
|
}
|
|
|
|
// GetTradingFees returns trading fees for all pairs based on trading activity
|
|
func (b *BTCMarkets) GetTradingFees(ctx context.Context) (TradingFeeResponse, error) {
|
|
var resp TradingFeeResponse
|
|
return resp, b.SendAuthenticatedRequest(ctx, http.MethodGet,
|
|
btcMarketsTradingFees,
|
|
nil,
|
|
&resp,
|
|
request.Auth)
|
|
}
|
|
|
|
// GetTradeHistory returns trade history
|
|
func (b *BTCMarkets) GetTradeHistory(ctx context.Context, marketID, orderID string, before, after, limit int64) ([]TradeHistoryData, error) {
|
|
if (before > 0) && (after >= 0) {
|
|
return nil, errors.New("BTCMarkets only supports either before or after, not both")
|
|
}
|
|
var resp []TradeHistoryData
|
|
params := url.Values{}
|
|
if marketID != "" {
|
|
params.Set("marketId", marketID)
|
|
}
|
|
if orderID != "" {
|
|
params.Set("orderId", orderID)
|
|
}
|
|
if before > 0 {
|
|
params.Set("before", strconv.FormatInt(before, 10))
|
|
}
|
|
if after >= 0 {
|
|
params.Set("after", strconv.FormatInt(after, 10))
|
|
}
|
|
if limit > 0 {
|
|
params.Set("limit", strconv.FormatInt(limit, 10))
|
|
}
|
|
return resp, b.SendAuthenticatedRequest(ctx, http.MethodGet,
|
|
common.EncodeURLValues(btcMarketsTradeHistory, params),
|
|
nil,
|
|
&resp,
|
|
request.Auth)
|
|
}
|
|
|
|
// GetTradeByID returns the singular trade of the ID given
|
|
func (b *BTCMarkets) GetTradeByID(ctx context.Context, id string) (TradeHistoryData, error) {
|
|
var resp TradeHistoryData
|
|
return resp, b.SendAuthenticatedRequest(ctx, http.MethodGet,
|
|
btcMarketsTradeHistory+"/"+id,
|
|
nil,
|
|
&resp,
|
|
request.Auth)
|
|
}
|
|
|
|
// formatOrderType conforms order type to the exchange acceptable order type
|
|
// strings
|
|
func (b *BTCMarkets) formatOrderType(o order.Type) (string, error) {
|
|
switch o {
|
|
case order.Limit:
|
|
return limit, nil
|
|
case order.Market:
|
|
return market, nil
|
|
case order.StopLimit:
|
|
return stopLimit, nil
|
|
case order.Stop:
|
|
return stop, nil
|
|
case order.TakeProfit:
|
|
return takeProfit, nil
|
|
default:
|
|
return "", fmt.Errorf("%s %s %w", b.Name, o, order.ErrTypeIsInvalid)
|
|
}
|
|
}
|
|
|
|
// formatOrderSide conforms order side to the exchange acceptable order side
|
|
// strings
|
|
func (b *BTCMarkets) formatOrderSide(o order.Side) (string, error) {
|
|
switch o {
|
|
case order.Ask:
|
|
return askSide, nil
|
|
case order.Bid:
|
|
return bidSide, nil
|
|
default:
|
|
return "", fmt.Errorf("%s %s %w", b.Name, o, order.ErrSideIsInvalid)
|
|
}
|
|
}
|
|
|
|
// getTimeInForce returns a string depending on the options in order.Submit
|
|
func (b *BTCMarkets) getTimeInForce(s *order.Submit) string {
|
|
if s.ImmediateOrCancel {
|
|
return immediateOrCancel
|
|
}
|
|
if s.FillOrKill {
|
|
return fillOrKill
|
|
}
|
|
return "" // GTC (good till cancelled, default value)
|
|
}
|
|
|
|
// NewOrder requests a new order and returns an ID
|
|
func (b *BTCMarkets) NewOrder(ctx context.Context, price, amount, triggerPrice, targetAmount float64, marketID, orderType, side, timeInForce, selfTrade, clientOrderID string, postOnly bool) (OrderData, error) {
|
|
req := make(map[string]interface{})
|
|
req["marketId"] = marketID
|
|
if price != 0 {
|
|
req["price"] = strconv.FormatFloat(price, 'f', -1, 64)
|
|
}
|
|
req["amount"] = strconv.FormatFloat(amount, 'f', -1, 64)
|
|
req["type"] = orderType
|
|
req["side"] = side
|
|
if orderType == stopLimit || orderType == takeProfit || orderType == stop {
|
|
req["triggerPrice"] = strconv.FormatFloat(triggerPrice, 'f', -1, 64)
|
|
}
|
|
if targetAmount > 0 {
|
|
req["targetAmount"] = strconv.FormatFloat(targetAmount, 'f', -1, 64)
|
|
}
|
|
if timeInForce != "" {
|
|
req["timeInForce"] = timeInForce
|
|
}
|
|
if postOnly {
|
|
req["postOnly"] = postOnly
|
|
}
|
|
if selfTrade != "" {
|
|
req["selfTrade"] = selfTrade
|
|
}
|
|
if clientOrderID != "" {
|
|
req["clientOrderID"] = clientOrderID
|
|
}
|
|
var resp OrderData
|
|
return resp, b.SendAuthenticatedRequest(ctx, http.MethodPost,
|
|
btcMarketsOrders,
|
|
req,
|
|
&resp,
|
|
orderFunc)
|
|
}
|
|
|
|
// GetOrders returns current order information on the exchange
|
|
func (b *BTCMarkets) GetOrders(ctx context.Context, marketID string, before, after, limit int64, openOnly bool) ([]OrderData, error) {
|
|
if (before > 0) && (after >= 0) {
|
|
return nil, errors.New("BTCMarkets only supports either before or after, not both")
|
|
}
|
|
var resp []OrderData
|
|
params := url.Values{}
|
|
if marketID != "" {
|
|
params.Set("marketId", marketID)
|
|
}
|
|
if before > 0 {
|
|
params.Set("before", strconv.FormatInt(before, 10))
|
|
}
|
|
if after >= 0 {
|
|
params.Set("after", strconv.FormatInt(after, 10))
|
|
}
|
|
if limit > 0 {
|
|
params.Set("limit", strconv.FormatInt(limit, 10))
|
|
}
|
|
if openOnly {
|
|
params.Set("status", "open")
|
|
}
|
|
return resp, b.SendAuthenticatedRequest(ctx, http.MethodGet,
|
|
common.EncodeURLValues(btcMarketsOrders, params),
|
|
nil,
|
|
&resp,
|
|
request.Auth)
|
|
}
|
|
|
|
// CancelAllOpenOrdersByPairs cancels all open orders unless pairs are specified
|
|
func (b *BTCMarkets) CancelAllOpenOrdersByPairs(ctx context.Context, marketIDs []string) ([]CancelOrderResp, error) {
|
|
var resp []CancelOrderResp
|
|
req := make(map[string]interface{})
|
|
if len(marketIDs) > 0 {
|
|
var strTemp strings.Builder
|
|
for x := range marketIDs {
|
|
strTemp.WriteString("marketId=" + marketIDs[x] + "&")
|
|
}
|
|
req["marketId"] = strTemp.String()[:strTemp.Len()-1]
|
|
}
|
|
return resp, b.SendAuthenticatedRequest(ctx, http.MethodDelete,
|
|
btcMarketsOrders,
|
|
req,
|
|
&resp,
|
|
request.Auth)
|
|
}
|
|
|
|
// FetchOrder finds order based on the provided id
|
|
func (b *BTCMarkets) FetchOrder(ctx context.Context, id string) (*OrderData, error) {
|
|
var resp *OrderData
|
|
return resp, b.SendAuthenticatedRequest(ctx, http.MethodGet,
|
|
btcMarketsOrders+"/"+id,
|
|
nil,
|
|
&resp,
|
|
request.Auth)
|
|
}
|
|
|
|
// RemoveOrder removes a given order
|
|
func (b *BTCMarkets) RemoveOrder(ctx context.Context, id string) (CancelOrderResp, error) {
|
|
var resp CancelOrderResp
|
|
return resp, b.SendAuthenticatedRequest(ctx, http.MethodDelete,
|
|
btcMarketsOrders+"/"+id,
|
|
nil,
|
|
&resp,
|
|
request.Auth)
|
|
}
|
|
|
|
// ReplaceOrder cancels an order and then places a new order.
|
|
func (b *BTCMarkets) ReplaceOrder(ctx context.Context, id, clientOrderID string, price, amount float64) (*OrderData, error) {
|
|
if price <= 0 {
|
|
return nil, fmt.Errorf("price %w", errInvalidAmount)
|
|
}
|
|
|
|
if amount <= 0 {
|
|
return nil, fmt.Errorf("amount %w", errInvalidAmount)
|
|
}
|
|
|
|
if id == "" {
|
|
return nil, errIDRequired
|
|
}
|
|
|
|
req := make(map[string]interface{}, 3)
|
|
req["price"] = strconv.FormatFloat(price, 'f', -1, 64)
|
|
req["amount"] = strconv.FormatFloat(amount, 'f', -1, 64)
|
|
if clientOrderID != "" {
|
|
req["clientOrderId"] = clientOrderID
|
|
}
|
|
|
|
var resp *OrderData
|
|
return resp, b.SendAuthenticatedRequest(ctx,
|
|
http.MethodPut,
|
|
btcMarketsOrders+"/"+id,
|
|
req,
|
|
&resp,
|
|
request.Auth)
|
|
}
|
|
|
|
// ListWithdrawals lists the withdrawal history
|
|
func (b *BTCMarkets) ListWithdrawals(ctx context.Context, before, after, limit int64) ([]TransferData, error) {
|
|
if (before > 0) && (after >= 0) {
|
|
return nil, errors.New("BTCMarkets only supports either before or after, not both")
|
|
}
|
|
var resp []TransferData
|
|
params := url.Values{}
|
|
if before > 0 {
|
|
params.Set("before", strconv.FormatInt(before, 10))
|
|
}
|
|
if after >= 0 {
|
|
params.Set("after", strconv.FormatInt(after, 10))
|
|
}
|
|
if limit > 0 {
|
|
params.Set("limit", strconv.FormatInt(limit, 10))
|
|
}
|
|
return resp, b.SendAuthenticatedRequest(ctx, http.MethodGet,
|
|
common.EncodeURLValues(btcMarketsWithdrawals, params),
|
|
nil,
|
|
&resp,
|
|
request.Auth)
|
|
}
|
|
|
|
// GetWithdrawal gets withdrawawl info for a given id
|
|
func (b *BTCMarkets) GetWithdrawal(ctx context.Context, id string) (TransferData, error) {
|
|
var resp TransferData
|
|
if id == "" {
|
|
return resp, errors.New("id cannot be an empty string")
|
|
}
|
|
return resp, b.SendAuthenticatedRequest(ctx, http.MethodGet,
|
|
btcMarketsWithdrawals+"/"+id,
|
|
nil,
|
|
&resp,
|
|
request.Auth)
|
|
}
|
|
|
|
// ListDeposits lists the deposit history
|
|
func (b *BTCMarkets) ListDeposits(ctx context.Context, before, after, limit int64) ([]TransferData, error) {
|
|
if (before > 0) && (after >= 0) {
|
|
return nil, errors.New("BTCMarkets only supports either before or after, not both")
|
|
}
|
|
var resp []TransferData
|
|
params := url.Values{}
|
|
if before > 0 {
|
|
params.Set("before", strconv.FormatInt(before, 10))
|
|
}
|
|
if after >= 0 {
|
|
params.Set("after", strconv.FormatInt(after, 10))
|
|
}
|
|
if limit > 0 {
|
|
params.Set("limit", strconv.FormatInt(limit, 10))
|
|
}
|
|
return resp, b.SendAuthenticatedRequest(ctx, http.MethodGet,
|
|
common.EncodeURLValues(btcMarketsDeposits, params),
|
|
nil,
|
|
&resp,
|
|
request.Auth)
|
|
}
|
|
|
|
// GetDeposit gets deposit info for a given ID
|
|
func (b *BTCMarkets) GetDeposit(ctx context.Context, id string) (TransferData, error) {
|
|
var resp TransferData
|
|
return resp, b.SendAuthenticatedRequest(ctx, http.MethodGet,
|
|
btcMarketsDeposits+"/"+id,
|
|
nil,
|
|
&resp,
|
|
request.Auth)
|
|
}
|
|
|
|
// ListTransfers lists the past asset transfers
|
|
func (b *BTCMarkets) ListTransfers(ctx context.Context, before, after, limit int64) ([]TransferData, error) {
|
|
if (before > 0) && (after >= 0) {
|
|
return nil, errors.New("BTCMarkets only supports either before or after, not both")
|
|
}
|
|
var resp []TransferData
|
|
params := url.Values{}
|
|
if before > 0 {
|
|
params.Set("before", strconv.FormatInt(before, 10))
|
|
}
|
|
if after >= 0 {
|
|
params.Set("after", strconv.FormatInt(after, 10))
|
|
}
|
|
if limit > 0 {
|
|
params.Set("limit", strconv.FormatInt(limit, 10))
|
|
}
|
|
return resp, b.SendAuthenticatedRequest(ctx, http.MethodGet,
|
|
common.EncodeURLValues(btcMarketsTransfers, params),
|
|
nil,
|
|
&resp,
|
|
request.Auth)
|
|
}
|
|
|
|
// GetTransfer gets asset transfer info for a given ID
|
|
func (b *BTCMarkets) GetTransfer(ctx context.Context, id string) (TransferData, error) {
|
|
var resp TransferData
|
|
return resp, b.SendAuthenticatedRequest(ctx, http.MethodGet,
|
|
btcMarketsTransfers+"/"+id,
|
|
nil,
|
|
&resp,
|
|
request.Auth)
|
|
}
|
|
|
|
// FetchDepositAddress gets deposit address for the given asset
|
|
func (b *BTCMarkets) FetchDepositAddress(ctx context.Context, curr currency.Code, before, after, limit int64) (*DepositAddress, error) {
|
|
var resp DepositAddress
|
|
if (before > 0) && (after >= 0) {
|
|
return nil, errors.New("BTCMarkets only supports either before or after, not both")
|
|
}
|
|
params := url.Values{}
|
|
params.Set("assetName", curr.Upper().String())
|
|
if before > 0 {
|
|
params.Set("before", strconv.FormatInt(before, 10))
|
|
}
|
|
if after >= 0 {
|
|
params.Set("after", strconv.FormatInt(after, 10))
|
|
}
|
|
if limit > 0 {
|
|
params.Set("limit", strconv.FormatInt(limit, 10))
|
|
}
|
|
if err := b.SendAuthenticatedRequest(ctx,
|
|
http.MethodGet,
|
|
common.EncodeURLValues(btcMarketsAddresses, params),
|
|
nil,
|
|
&resp,
|
|
request.Auth); err != nil {
|
|
return nil, err
|
|
}
|
|
if curr.Equal(currency.XRP) {
|
|
splitStr := "?dt="
|
|
if !strings.Contains(resp.Address, splitStr) {
|
|
return nil, errors.New("unable to find split string for XRP")
|
|
}
|
|
splitter := strings.Split(resp.Address, splitStr)
|
|
resp.Address = splitter[0]
|
|
resp.Tag = splitter[1]
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
// GetWithdrawalFees gets withdrawal fees for all assets
|
|
func (b *BTCMarkets) GetWithdrawalFees(ctx context.Context) ([]WithdrawalFeeData, error) {
|
|
var resp []WithdrawalFeeData
|
|
return resp, b.SendHTTPRequest(ctx, btcMarketsAPIURL+btcMarketsAPIVersion+btcMarketsWithdrawalFees,
|
|
&resp)
|
|
}
|
|
|
|
// ListAssets lists all available assets
|
|
func (b *BTCMarkets) ListAssets(ctx context.Context) ([]AssetData, error) {
|
|
var resp []AssetData
|
|
return resp, b.SendAuthenticatedRequest(ctx, http.MethodGet,
|
|
btcMarketsAssets,
|
|
nil,
|
|
&resp,
|
|
request.Auth)
|
|
}
|
|
|
|
// GetTransactions gets trading fees
|
|
func (b *BTCMarkets) GetTransactions(ctx context.Context, assetName string, before, after, limit int64) ([]TransactionData, error) {
|
|
if (before > 0) && (after >= 0) {
|
|
return nil, errors.New("BTCMarkets only supports either before or after, not both")
|
|
}
|
|
var resp []TransactionData
|
|
params := url.Values{}
|
|
if assetName != "" {
|
|
params.Set("assetName", assetName)
|
|
}
|
|
if before > 0 {
|
|
params.Set("before", strconv.FormatInt(before, 10))
|
|
}
|
|
if after >= 0 {
|
|
params.Set("after", strconv.FormatInt(after, 10))
|
|
}
|
|
if limit > 0 {
|
|
params.Set("limit", strconv.FormatInt(limit, 10))
|
|
}
|
|
return resp, b.SendAuthenticatedRequest(ctx, http.MethodGet,
|
|
common.EncodeURLValues(btcMarketsTransactions, params),
|
|
nil,
|
|
&resp,
|
|
request.Auth)
|
|
}
|
|
|
|
// CreateNewReport creates a new report
|
|
func (b *BTCMarkets) CreateNewReport(ctx context.Context, reportType, format string) (CreateReportResp, error) {
|
|
var resp CreateReportResp
|
|
req := make(map[string]interface{})
|
|
req["type"] = reportType
|
|
req["format"] = format
|
|
return resp, b.SendAuthenticatedRequest(ctx, http.MethodPost,
|
|
btcMarketsReports,
|
|
req,
|
|
&resp,
|
|
newReportFunc)
|
|
}
|
|
|
|
// GetReport finds details bout a past report
|
|
func (b *BTCMarkets) GetReport(ctx context.Context, reportID string) (ReportData, error) {
|
|
var resp ReportData
|
|
return resp, b.SendAuthenticatedRequest(ctx, http.MethodGet,
|
|
btcMarketsReports+"/"+reportID,
|
|
nil,
|
|
&resp,
|
|
request.Auth)
|
|
}
|
|
|
|
// RequestWithdraw requests withdrawals
|
|
func (b *BTCMarkets) RequestWithdraw(ctx context.Context, assetName string, amount float64,
|
|
toAddress, accountName, accountNumber, bsbNumber, bankName string) (TransferData, error) {
|
|
var resp TransferData
|
|
req := make(map[string]interface{})
|
|
req["assetName"] = assetName
|
|
req["amount"] = strconv.FormatFloat(amount, 'f', -1, 64)
|
|
if assetName != "AUD" {
|
|
req["toAddress"] = toAddress
|
|
} else {
|
|
if accountName != "" {
|
|
req["accountName"] = accountName
|
|
}
|
|
if accountNumber != "" {
|
|
req["accountNumber"] = accountNumber
|
|
}
|
|
if bsbNumber != "" {
|
|
req["bsbNumber"] = bsbNumber
|
|
}
|
|
if bankName != "" {
|
|
req["bankName"] = bankName
|
|
}
|
|
}
|
|
return resp, b.SendAuthenticatedRequest(ctx, http.MethodPost,
|
|
btcMarketsWithdrawals,
|
|
req,
|
|
&resp,
|
|
withdrawFunc)
|
|
}
|
|
|
|
// BatchPlaceCancelOrders places and cancels batch orders
|
|
func (b *BTCMarkets) BatchPlaceCancelOrders(ctx context.Context, cancelOrders []CancelBatch, placeOrders []PlaceBatch) (*BatchPlaceCancelResponse, error) {
|
|
numActions := len(cancelOrders) + len(placeOrders)
|
|
if numActions > 4 {
|
|
return nil, errors.New("BTCMarkets can only handle 4 orders at a time")
|
|
}
|
|
|
|
orderRequests := make([]interface{}, numActions)
|
|
for x := range cancelOrders {
|
|
orderRequests[x] = CancelOrderMethod{CancelOrder: cancelOrders[x]}
|
|
}
|
|
for y := range placeOrders {
|
|
if placeOrders[y].ClientOrderID == "" {
|
|
return nil, errors.New("placeorders must have ClientOrderID filled")
|
|
}
|
|
orderRequests[y] = PlaceOrderMethod{PlaceOrder: placeOrders[y]}
|
|
}
|
|
var resp BatchPlaceCancelResponse
|
|
return &resp, b.SendAuthenticatedRequest(ctx, http.MethodPost,
|
|
btcMarketsBatchOrders,
|
|
orderRequests,
|
|
&resp,
|
|
batchFunc)
|
|
}
|
|
|
|
// GetBatchTrades gets batch trades
|
|
func (b *BTCMarkets) GetBatchTrades(ctx context.Context, ids []string) (BatchTradeResponse, error) {
|
|
var resp BatchTradeResponse
|
|
if len(ids) > 50 {
|
|
return resp, errors.New("batchtrades can only handle 50 ids at a time")
|
|
}
|
|
marketIDs := strings.Join(ids, ",")
|
|
return resp, b.SendAuthenticatedRequest(ctx, http.MethodGet,
|
|
btcMarketsBatchOrders+"/"+marketIDs,
|
|
nil,
|
|
&resp,
|
|
request.Auth)
|
|
}
|
|
|
|
// CancelBatch cancels given ids
|
|
func (b *BTCMarkets) CancelBatch(ctx context.Context, ids []string) (BatchCancelResponse, error) {
|
|
var resp BatchCancelResponse
|
|
marketIDs := strings.Join(ids, ",")
|
|
return resp, b.SendAuthenticatedRequest(ctx, http.MethodDelete,
|
|
btcMarketsBatchOrders+"/"+marketIDs,
|
|
nil,
|
|
&resp,
|
|
batchFunc)
|
|
}
|
|
|
|
// SendHTTPRequest sends an unauthenticated HTTP request
|
|
func (b *BTCMarkets) SendHTTPRequest(ctx context.Context, path string, result interface{}) error {
|
|
item := &request.Item{
|
|
Method: http.MethodGet,
|
|
Path: path,
|
|
Result: result,
|
|
Verbose: b.Verbose,
|
|
HTTPDebugging: b.HTTPDebugging,
|
|
HTTPRecording: b.HTTPRecording,
|
|
}
|
|
return b.SendPayload(ctx, request.UnAuth, func() (*request.Item, error) {
|
|
return item, nil
|
|
}, request.UnauthenticatedRequest)
|
|
}
|
|
|
|
// SendAuthenticatedRequest sends an authenticated HTTP request
|
|
func (b *BTCMarkets) SendAuthenticatedRequest(ctx context.Context, method, path string, data, result interface{}, f request.EndpointLimit) (err error) {
|
|
creds, err := b.GetCredentials(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
newRequest := func() (*request.Item, error) {
|
|
strTime := strconv.FormatInt(time.Now().UnixMilli(), 10)
|
|
var body io.Reader
|
|
var payload, hmac []byte
|
|
switch data.(type) {
|
|
case map[string]interface{}, []interface{}:
|
|
payload, err = json.Marshal(data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
body = bytes.NewBuffer(payload)
|
|
strMsg := method + btcMarketsAPIVersion + path + strTime + string(payload)
|
|
hmac, err = crypto.GetHMAC(crypto.HashSHA512,
|
|
[]byte(strMsg),
|
|
[]byte(creds.Secret))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
default:
|
|
strArray := strings.Split(path, "?")
|
|
hmac, err = crypto.GetHMAC(crypto.HashSHA512,
|
|
[]byte(method+btcMarketsAPIVersion+strArray[0]+strTime),
|
|
[]byte(creds.Secret))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
headers := make(map[string]string)
|
|
headers["Accept"] = "application/json"
|
|
headers["Accept-Charset"] = "UTF-8"
|
|
headers["Content-Type"] = "application/json"
|
|
headers["BM-AUTH-APIKEY"] = creds.Key
|
|
headers["BM-AUTH-TIMESTAMP"] = strTime
|
|
headers["BM-AUTH-SIGNATURE"] = crypto.Base64Encode(hmac)
|
|
|
|
return &request.Item{
|
|
Method: method,
|
|
Path: btcMarketsAPIURL + btcMarketsAPIVersion + path,
|
|
Headers: headers,
|
|
Body: body,
|
|
Result: result,
|
|
Verbose: b.Verbose,
|
|
HTTPDebugging: b.HTTPDebugging,
|
|
HTTPRecording: b.HTTPRecording,
|
|
}, nil
|
|
}
|
|
|
|
return b.SendPayload(ctx, f, newRequest, request.AuthenticatedRequest)
|
|
}
|
|
|
|
// GetFee returns an estimate of fee based on type of transaction
|
|
func (b *BTCMarkets) GetFee(ctx context.Context, feeBuilder *exchange.FeeBuilder) (float64, error) {
|
|
var fee float64
|
|
|
|
switch feeBuilder.FeeType {
|
|
case exchange.CryptocurrencyTradeFee:
|
|
temp, err := b.GetTradingFees(ctx)
|
|
if err != nil {
|
|
return fee, err
|
|
}
|
|
for x := range temp.FeeByMarkets {
|
|
p, err := currency.NewPairFromString(temp.FeeByMarkets[x].MarketID)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if p == feeBuilder.Pair {
|
|
fee = temp.FeeByMarkets[x].MakerFeeRate
|
|
if !feeBuilder.IsMaker {
|
|
fee = temp.FeeByMarkets[x].TakerFeeRate
|
|
}
|
|
}
|
|
}
|
|
case exchange.CryptocurrencyWithdrawalFee:
|
|
temp, err := b.GetWithdrawalFees(ctx)
|
|
if err != nil {
|
|
return fee, err
|
|
}
|
|
for x := range temp {
|
|
if currency.NewCode(temp[x].AssetName) == feeBuilder.Pair.Base {
|
|
fee = temp[x].Fee * feeBuilder.PurchasePrice * feeBuilder.Amount
|
|
}
|
|
}
|
|
case exchange.InternationalBankWithdrawalFee:
|
|
return 0, errors.New("international bank withdrawals are not supported")
|
|
|
|
case exchange.OfflineTradeFee:
|
|
fee = getOfflineTradeFee(feeBuilder)
|
|
}
|
|
if fee < 0 {
|
|
fee = 0
|
|
}
|
|
return fee, nil
|
|
}
|
|
|
|
// getOfflineTradeFee calculates the worst case-scenario trading fee
|
|
func getOfflineTradeFee(feeBuilder *exchange.FeeBuilder) float64 {
|
|
switch {
|
|
case feeBuilder.Pair.IsCryptoPair():
|
|
return 0.002 * feeBuilder.PurchasePrice * feeBuilder.Amount
|
|
default:
|
|
return 0.0085 * feeBuilder.PurchasePrice * feeBuilder.Amount
|
|
}
|
|
}
|