Files
gocryptotrader/exchanges/btcmarkets/btcmarkets.go
Ryan O'Hara-Reid 16a93b49a4 btcm: add modify order functionality, change return to pointer (#940)
* btcm: add modify order functionality, change return to pointer

* glorious: nits

* glorious: nits

* btcm: Adjust function name

* thrasher: nits

* thrasher: nits cont...
2022-05-16 10:55:23 +10:00

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"
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"
wsOB = "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 == 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.Unset, func() (*request.Item, error) {
return item, nil
})
}
// 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,
AuthRequest: true,
Verbose: b.Verbose,
HTTPDebugging: b.HTTPDebugging,
HTTPRecording: b.HTTPRecording,
}, nil
}
return b.SendPayload(ctx, f, newRequest)
}
// 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
}
}