Files
gocryptotrader/exchanges/binance/binance.go
Adam 504c2fad6d Feature: Implement funding rates, futures and coin margin (exchange API coverage) (#530)
* ALMOST THERE

* more api wips

* more api thingz

* testing n more api wipz

* more apiz

* more wips

* what is goin on

* more wips

* whip n testing

* testing

* testing

no keys

* remove log

* kraken is broken

ugh

* still broken

* fixing auth funcs + usdtm api docs

* wip

* api stuffs

* whip

* more wips

* whip

* more wip

* api wip n testing

* wip

* wip

* unsaved

* wip n testing

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* whip

* wrapper authenticated functions

* adding asset type and fixing dependencies

* wip

* binance auth wrapper start

* wrapper functionality

* wip

* wip

* wip

* wrapper cancel functions

* order submission for wrappers

* wip

* more error fixing and nits

* websocket beginning n error fix

* wip

* WOW

* glorious n shazzy nits

* useless nits

* wip

* fixing things

* merge stuffs

* crapveyor

* crapveyor rebuild

* probably broke more things than he fixed

* rm lns n other thangs

* hope

* please

* stop it

* done

* ofcourse

* rm vb

* fix lbank

* appveyor please

* float lev

* DONT ASK RYAN FOR HELP EVER

* wip

* wip

* endpoint upgrades continued

* path upgrade

* NeeeNeeeNeeeNeeeNING

* fix stuffs

* fixing time issue

* fixing broken funcs

* glorious nits

* shaz changes

* fixing errors for fundmon

* more error fixing for fundmon

* test running past 30s

* basic changes

* THX AGAIN SHAZBERT

* path system upgrade

* config upgrade

* unsaved stuffs

* broken wip config upgrade

* path system upgrade contd.

* path system upgrade contd

* path upgrade ready for review

* testing verbose removed

* linter stuffs

* appveyor stuffs

* appveyor stuff

* fixed?

* bugfix

* wip

* broken stuff

* fix test

* wierd hack fix

* appveyor pls stop

* error found

* more useless nits

* bitmex err

* broken wip

* broken wip path upgrade change to uint32

* changed url lookups to uint

* WOW

* ready4review

* config fixed HOPEFULLY

* config fix and glorious changes

* efficient way of getting orders and open orders

* binance wrapper logic fixing

* testing, adding tests and fixing lot of errrrrs

* merge master

* appveyor stuffs

* appveyor stuffs

* fmt

* test

* octalLiteral issue fix?

* octalLiteral fix?

* rm vb

* prnt ln to restart

* adding testz

* test fixzzz

* READY FOR REVIEW

* Actually ready now

* FORMATTING

* addressing shazzy n glorious nits

* crapveyor

* rm vb

* small change

* fixing err

* shazbert nits

* review changes

* requested changes

* more requested changes

* noo

* last nit fixes

* restart appveyor

* improving test cov

* Update .golangci.yml

* shazbert changes

* moving pair formatting

* format pair update wip

* path upgrade complete

* error fix

* appveyor linters

* more linters

* remove testexch

* more formatting changes

* changes

* shazbert changes

* checking older requested changes to ensure completion

* wip

* fixing broken code

* error fix

* all fixed

* additional changes

* more changes

* remove commented code

* ftx margin api

* appveyor fixes

* more appveyor issues + test addition

* more appveyor issues + test addition

* remove unnecessary

* testing

* testing, fixing okex api, error fix

* git merge fix

* go sum

* glorious changes and error fix

* rm vb

* more glorious changes and go mod tidy

* fixed now

* okex testing upgrade

* old config migration and batch fetching fix

* added test

* glorious requested changes WIP

* tested and fixed

* go fmted

* go fmt and test fix

* additional funcs and tests for fundingRates

* OKEX tested and fixed

* appveyor fixes

* ineff assign

* 1 glorious change

* error fix

* typo

* shazbert changes

* glorious code changes and path fixing huobi WIP

* adding assetType to accountinfo functions

* fixing panic

* panic fix and updating account info wrappers WIP

* updateaccountinfo updated

* testing WIP binance USDT n Coin Margined and Kraken Futures

* auth functions tested and fixed

* added test

* config reverted

* shazbert and glorious changes

* shazbert and glorious changes

* latest changes and portfolio update

* go fmt change:

* remove commented codes

* improved error checking

* index out of range fix

* rm ln

* critical nit

* glorious latest changes

* appveyor changes

* shazbert change

* easier readability

* latest glorious changes

* shadow dec

* assetstore updated

* last change

* another last change

* merge changes

* go mod tidy

* thrasher requested changes wip

* improving struct layouts

* appveyor go fmt

* remove unnecessary code

* shazbert changes

* small change

* oopsie

* tidy

* configtest reverted

* error fix

* oopsie

* for what

* test patch fix

* insecurities

* fixing tests

* fix config
2021-02-12 16:19:18 +11:00

925 lines
28 KiB
Go

package binance
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"sort"
"strconv"
"time"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/common/convert"
"github.com/thrasher-corp/gocryptotrader/common/crypto"
"github.com/thrasher-corp/gocryptotrader/currency"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"github.com/thrasher-corp/gocryptotrader/log"
)
// Binance is the overarching type across the Binance package
type Binance struct {
exchange.Base
// Valid string list that is required by the exchange
validLimits []int
obm *orderbookManager
}
const (
apiURL = "https://api.binance.com"
spotAPIURL = "https://sapi.binance.com"
cfuturesAPIURL = "https://dapi.binance.com"
ufuturesAPIURL = "https://fapi.binance.com"
// Public endpoints
exchangeInfo = "/api/v3/exchangeInfo"
orderBookDepth = "/api/v3/depth"
recentTrades = "/api/v3/trades"
aggregatedTrades = "/api/v3/aggTrades"
candleStick = "/api/v3/klines"
averagePrice = "/api/v3/avgPrice"
priceChange = "/api/v3/ticker/24hr"
symbolPrice = "/api/v3/ticker/price"
bestPrice = "/api/v3/ticker/bookTicker"
accountInfo = "/api/v3/account"
userAccountStream = "/api/v3/userDataStream"
perpExchangeInfo = "/fapi/v1/exchangeInfo"
// Authenticated endpoints
newOrderTest = "/api/v3/order/test"
orderEndpoint = "/api/v3/order"
openOrders = "/api/v3/openOrders"
allOrders = "/api/v3/allOrders"
// Withdraw API endpoints
withdrawEndpoint = "/wapi/v3/withdraw.html"
depositHistory = "/wapi/v3/depositHistory.html"
withdrawalHistory = "/wapi/v3/withdrawHistory.html"
depositAddress = "/wapi/v3/depositAddress.html"
accountStatus = "/wapi/v3/accountStatus.html"
systemStatus = "/wapi/v3/systemStatus.html"
dustLog = "/wapi/v3/userAssetDribbletLog.html"
tradeFee = "/wapi/v3/tradeFee.html"
assetDetail = "/wapi/v3/assetDetail.html"
undocumentedInterestHistory = "/gateway-api/v1/public/isolated-margin/pair/vip-level"
undocumentedCrossMarginInterestHistory = "/gateway-api/v1/friendly/margin/vip/spec/list-all"
)
// GetInterestHistory gets interest history for currency/currencies provided
func (b *Binance) GetInterestHistory() (MarginInfoData, error) {
var resp MarginInfoData
if err := b.SendHTTPRequest(exchange.EdgeCase1, undocumentedInterestHistory, limitDefault, &resp); err != nil {
return resp, err
}
return resp, nil
}
// GetCrossMarginInterestHistory gets cross-margin interest history for currency/currencies provided
func (b *Binance) GetCrossMarginInterestHistory() (CrossMarginInterestData, error) {
var resp CrossMarginInterestData
if err := b.SendHTTPRequest(exchange.EdgeCase1, undocumentedCrossMarginInterestHistory, limitDefault, &resp); err != nil {
return resp, err
}
return resp, nil
}
// GetMarginMarkets returns exchange information. Check binance_types for more information
func (b *Binance) GetMarginMarkets() (PerpsExchangeInfo, error) {
var resp PerpsExchangeInfo
return resp, b.SendHTTPRequest(exchange.RestSpot, perpExchangeInfo, limitDefault, &resp)
}
// GetExchangeInfo returns exchange information. Check binance_types for more
// information
func (b *Binance) GetExchangeInfo() (ExchangeInfo, error) {
var resp ExchangeInfo
return resp, b.SendHTTPRequest(exchange.RestSpotSupplementary, exchangeInfo, limitDefault, &resp)
}
// GetOrderBook returns full orderbook information
//
// OrderBookDataRequestParams contains the following members
// symbol: string of currency pair
// limit: returned limit amount
func (b *Binance) GetOrderBook(obd OrderBookDataRequestParams) (OrderBook, error) {
var orderbook OrderBook
if err := b.CheckLimit(obd.Limit); err != nil {
return orderbook, err
}
params := url.Values{}
symbol, err := b.FormatSymbol(obd.Symbol, asset.Spot)
if err != nil {
return orderbook, err
}
params.Set("symbol", symbol)
params.Set("limit", fmt.Sprintf("%d", obd.Limit))
var resp OrderBookData
if err := b.SendHTTPRequest(exchange.RestSpotSupplementary, orderBookDepth+"?"+params.Encode(), orderbookLimit(obd.Limit), &resp); err != nil {
return orderbook, err
}
for x := range resp.Bids {
price, err := strconv.ParseFloat(resp.Bids[x][0], 64)
if err != nil {
return orderbook, err
}
amount, err := strconv.ParseFloat(resp.Bids[x][1], 64)
if err != nil {
return orderbook, err
}
orderbook.Bids = append(orderbook.Bids, OrderbookItem{
Price: price,
Quantity: amount,
})
}
for x := range resp.Asks {
price, err := strconv.ParseFloat(resp.Asks[x][0], 64)
if err != nil {
return orderbook, err
}
amount, err := strconv.ParseFloat(resp.Asks[x][1], 64)
if err != nil {
return orderbook, err
}
orderbook.Asks = append(orderbook.Asks, OrderbookItem{
Price: price,
Quantity: amount,
})
}
orderbook.LastUpdateID = resp.LastUpdateID
return orderbook, nil
}
// GetMostRecentTrades returns recent trade activity
// limit: Up to 500 results returned
func (b *Binance) GetMostRecentTrades(rtr RecentTradeRequestParams) ([]RecentTrade, error) {
var resp []RecentTrade
params := url.Values{}
symbol, err := b.FormatSymbol(rtr.Symbol, asset.Spot)
if err != nil {
return nil, err
}
params.Set("symbol", symbol)
params.Set("limit", fmt.Sprintf("%d", rtr.Limit))
path := recentTrades + "?" + params.Encode()
return resp, b.SendHTTPRequest(exchange.RestSpotSupplementary, path, limitDefault, &resp)
}
// GetHistoricalTrades returns historical trade activity
//
// symbol: string of currency pair
// limit: Optional. Default 500; max 1000.
// fromID:
func (b *Binance) GetHistoricalTrades(symbol string, limit int, fromID int64) ([]HistoricalTrade, error) {
// Dropping support due to response for market data is always
// {"code":-2014,"msg":"API-key format invalid."}
// TODO: replace with newer API vs REST endpoint
return nil, common.ErrFunctionNotSupported
}
// GetAggregatedTrades returns aggregated trade activity.
// If more than one hour of data is requested or asked limit is not supported by exchange
// then the trades are collected with multiple backend requests.
// https://binance-docs.github.io/apidocs/spot/en/#compressed-aggregate-trades-list
func (b *Binance) GetAggregatedTrades(arg *AggregatedTradeRequestParams) ([]AggregatedTrade, error) {
params := url.Values{}
symbol, err := b.FormatSymbol(arg.Symbol, asset.Spot)
if err != nil {
return nil, err
}
params.Set("symbol", symbol)
// if the user request is directly not supported by the exchange, we might be able to fulfill it
// by merging results from multiple API requests
needBatch := false
if arg.Limit > 0 {
if arg.Limit > 1000 {
// remote call doesn't support higher limits
needBatch = true
} else {
params.Set("limit", strconv.Itoa(arg.Limit))
}
}
if arg.FromID != 0 {
params.Set("fromId", strconv.FormatInt(arg.FromID, 10))
}
if !arg.StartTime.IsZero() {
params.Set("startTime", timeString(arg.StartTime))
}
if !arg.EndTime.IsZero() {
params.Set("endTime", timeString(arg.EndTime))
}
// startTime and endTime are set and time between startTime and endTime is more than 1 hour
needBatch = needBatch || (!arg.StartTime.IsZero() && !arg.EndTime.IsZero() && arg.EndTime.Sub(arg.StartTime) > time.Hour)
// Fall back to batch requests, if possible and necessary
if needBatch {
// fromId xor start time must be set
canBatch := arg.FromID == 0 != arg.StartTime.IsZero()
if canBatch {
// Split the request into multiple
return b.batchAggregateTrades(arg, params)
}
// Can't handle this request locally or remotely
// We would receive {"code":-1128,"msg":"Combination of optional parameters invalid."}
return nil, errors.New("please set StartTime or FromId, but not both")
}
var resp []AggregatedTrade
path := aggregatedTrades + "?" + params.Encode()
return resp, b.SendHTTPRequest(exchange.RestSpotSupplementary, path, limitDefault, &resp)
}
// batchAggregateTrades fetches trades in multiple requests
// first phase, hourly requests until the first trade (or end time) is reached
// second phase, limit requests from previous trade until end time (or limit) is reached
func (b *Binance) batchAggregateTrades(arg *AggregatedTradeRequestParams, params url.Values) ([]AggregatedTrade, error) {
var resp []AggregatedTrade
// prepare first request with only first hour and max limit
if arg.Limit == 0 || arg.Limit > 1000 {
// Extend from the default of 500
params.Set("limit", "1000")
}
var fromID int64
if arg.FromID > 0 {
fromID = arg.FromID
} else {
for start := arg.StartTime; len(resp) == 0; start = start.Add(time.Hour) {
if !arg.EndTime.IsZero() && !start.Before(arg.EndTime) {
// All requests returned empty
return nil, nil
}
params.Set("startTime", timeString(start))
params.Set("endTime", timeString(start.Add(time.Hour)))
path := aggregatedTrades + "?" + params.Encode()
err := b.SendHTTPRequest(exchange.RestSpotSupplementary, path, limitDefault, &resp)
if err != nil {
log.Warn(log.ExchangeSys, err.Error())
return resp, err
}
}
fromID = resp[len(resp)-1].ATradeID
}
// other requests follow from the last aggregate trade id and have no time window
params.Del("startTime")
params.Del("endTime")
// while we haven't reached the limit
for ; arg.Limit == 0 || len(resp) < arg.Limit; fromID = resp[len(resp)-1].ATradeID {
// Keep requesting new data after last retrieved trade
params.Set("fromId", strconv.FormatInt(fromID, 10))
path := aggregatedTrades + "?" + params.Encode()
var additionalTrades []AggregatedTrade
err := b.SendHTTPRequest(exchange.RestSpotSupplementary, path, limitDefault, &additionalTrades)
if err != nil {
return resp, err
}
lastIndex := len(additionalTrades)
if !arg.EndTime.IsZero() {
// get index for truncating to end time
lastIndex = sort.Search(len(additionalTrades), func(i int) bool {
return arg.EndTime.Before(additionalTrades[i].TimeStamp)
})
}
// don't include the first as the request was inclusive from last ATradeID
resp = append(resp, additionalTrades[1:lastIndex]...)
// If only the starting trade is returned or if we received trades after end time
if len(additionalTrades) == 1 || lastIndex < len(additionalTrades) {
// We found the end
break
}
}
// Truncate if necessary
if arg.Limit > 0 && len(resp) > arg.Limit {
resp = resp[:arg.Limit]
}
return resp, nil
}
// GetSpotKline returns kline data
//
// KlinesRequestParams supports 5 parameters
// symbol: the symbol to get the kline data for
// limit: optinal
// interval: the interval time for the data
// startTime: startTime filter for kline data
// endTime: endTime filter for the kline data
func (b *Binance) GetSpotKline(arg *KlinesRequestParams) ([]CandleStick, error) {
var resp interface{}
var klineData []CandleStick
params := url.Values{}
symbol, err := b.FormatSymbol(arg.Symbol, asset.Spot)
if err != nil {
return nil, err
}
params.Set("symbol", symbol)
params.Set("interval", arg.Interval)
if arg.Limit != 0 {
params.Set("limit", strconv.Itoa(arg.Limit))
}
if !arg.StartTime.IsZero() {
params.Set("startTime", timeString(arg.StartTime))
}
if !arg.EndTime.IsZero() {
params.Set("endTime", timeString(arg.EndTime))
}
path := candleStick + "?" + params.Encode()
if err := b.SendHTTPRequest(exchange.RestSpotSupplementary, path, limitDefault, &resp); err != nil {
return klineData, err
}
for _, responseData := range resp.([]interface{}) {
var candle CandleStick
for i, individualData := range responseData.([]interface{}) {
switch i {
case 0:
tempTime := individualData.(float64)
var err error
candle.OpenTime, err = convert.TimeFromUnixTimestampFloat(tempTime)
if err != nil {
return klineData, err
}
case 1:
candle.Open, _ = strconv.ParseFloat(individualData.(string), 64)
case 2:
candle.High, _ = strconv.ParseFloat(individualData.(string), 64)
case 3:
candle.Low, _ = strconv.ParseFloat(individualData.(string), 64)
case 4:
candle.Close, _ = strconv.ParseFloat(individualData.(string), 64)
case 5:
candle.Volume, _ = strconv.ParseFloat(individualData.(string), 64)
case 6:
tempTime := individualData.(float64)
var err error
candle.CloseTime, err = convert.TimeFromUnixTimestampFloat(tempTime)
if err != nil {
return klineData, err
}
case 7:
candle.QuoteAssetVolume, _ = strconv.ParseFloat(individualData.(string), 64)
case 8:
candle.TradeCount = individualData.(float64)
case 9:
candle.TakerBuyAssetVolume, _ = strconv.ParseFloat(individualData.(string), 64)
case 10:
candle.TakerBuyQuoteAssetVolume, _ = strconv.ParseFloat(individualData.(string), 64)
}
}
klineData = append(klineData, candle)
}
return klineData, nil
}
// GetAveragePrice returns current average price for a symbol.
//
// symbol: string of currency pair
func (b *Binance) GetAveragePrice(symbol currency.Pair) (AveragePrice, error) {
resp := AveragePrice{}
params := url.Values{}
symbolValue, err := b.FormatSymbol(symbol, asset.Spot)
if err != nil {
return resp, err
}
params.Set("symbol", symbolValue)
path := averagePrice + "?" + params.Encode()
return resp, b.SendHTTPRequest(exchange.RestSpotSupplementary, path, limitDefault, &resp)
}
// GetPriceChangeStats returns price change statistics for the last 24 hours
//
// symbol: string of currency pair
func (b *Binance) GetPriceChangeStats(symbol currency.Pair) (PriceChangeStats, error) {
resp := PriceChangeStats{}
params := url.Values{}
symbolValue, err := b.FormatSymbol(symbol, asset.Spot)
if err != nil {
return resp, err
}
params.Set("symbol", symbolValue)
path := priceChange + "?" + params.Encode()
return resp, b.SendHTTPRequest(exchange.RestSpotSupplementary, path, limitDefault, &resp)
}
// GetTickers returns the ticker data for the last 24 hrs
func (b *Binance) GetTickers() ([]PriceChangeStats, error) {
var resp []PriceChangeStats
return resp, b.SendHTTPRequest(exchange.RestSpotSupplementary, priceChange, limitPriceChangeAll, &resp)
}
// GetLatestSpotPrice returns latest spot price of symbol
//
// symbol: string of currency pair
func (b *Binance) GetLatestSpotPrice(symbol currency.Pair) (SymbolPrice, error) {
resp := SymbolPrice{}
params := url.Values{}
symbolValue, err := b.FormatSymbol(symbol, asset.Spot)
if err != nil {
return resp, err
}
params.Set("symbol", symbolValue)
path := symbolPrice + "?" + params.Encode()
return resp, b.SendHTTPRequest(exchange.RestSpotSupplementary, path, symbolPriceLimit(symbolValue), &resp)
}
// GetBestPrice returns the latest best price for symbol
//
// symbol: string of currency pair
func (b *Binance) GetBestPrice(symbol currency.Pair) (BestPrice, error) {
resp := BestPrice{}
params := url.Values{}
symbolValue, err := b.FormatSymbol(symbol, asset.Spot)
if err != nil {
return resp, err
}
params.Set("symbol", symbolValue)
path := bestPrice + "?" + params.Encode()
return resp, b.SendHTTPRequest(exchange.RestSpotSupplementary, path, bestPriceLimit(symbolValue), &resp)
}
// NewOrder sends a new order to Binance
func (b *Binance) NewOrder(o *NewOrderRequest) (NewOrderResponse, error) {
var resp NewOrderResponse
if err := b.newOrder(orderEndpoint, o, &resp); err != nil {
return resp, err
}
if resp.Code != 0 {
return resp, errors.New(resp.Msg)
}
return resp, nil
}
// NewOrderTest sends a new test order to Binance
func (b *Binance) NewOrderTest(o *NewOrderRequest) error {
var resp NewOrderResponse
return b.newOrder(newOrderTest, o, &resp)
}
func (b *Binance) newOrder(api string, o *NewOrderRequest, resp *NewOrderResponse) error {
params := url.Values{}
symbol, err := b.FormatSymbol(o.Symbol, asset.Spot)
if err != nil {
return err
}
params.Set("symbol", symbol)
params.Set("side", o.Side)
params.Set("type", string(o.TradeType))
if o.QuoteOrderQty > 0 {
params.Set("quoteOrderQty", strconv.FormatFloat(o.QuoteOrderQty, 'f', -1, 64))
} else {
params.Set("quantity", strconv.FormatFloat(o.Quantity, 'f', -1, 64))
}
if o.TradeType == BinanceRequestParamsOrderLimit {
params.Set("price", strconv.FormatFloat(o.Price, 'f', -1, 64))
}
if o.TimeInForce != "" {
params.Set("timeInForce", string(o.TimeInForce))
}
if o.NewClientOrderID != "" {
params.Set("newClientOrderID", o.NewClientOrderID)
}
if o.StopPrice != 0 {
params.Set("stopPrice", strconv.FormatFloat(o.StopPrice, 'f', -1, 64))
}
if o.IcebergQty != 0 {
params.Set("icebergQty", strconv.FormatFloat(o.IcebergQty, 'f', -1, 64))
}
if o.NewOrderRespType != "" {
params.Set("newOrderRespType", o.NewOrderRespType)
}
return b.SendAuthHTTPRequest(exchange.RestSpotSupplementary, http.MethodPost, api, params, limitOrder, resp)
}
// CancelExistingOrder sends a cancel order to Binance
func (b *Binance) CancelExistingOrder(symbol currency.Pair, orderID int64, origClientOrderID string) (CancelOrderResponse, error) {
var resp CancelOrderResponse
symbolValue, err := b.FormatSymbol(symbol, asset.Spot)
if err != nil {
return resp, err
}
params := url.Values{}
params.Set("symbol", symbolValue)
if orderID != 0 {
params.Set("orderId", strconv.FormatInt(orderID, 10))
}
if origClientOrderID != "" {
params.Set("origClientOrderId", origClientOrderID)
}
return resp, b.SendAuthHTTPRequest(exchange.RestSpotSupplementary, http.MethodDelete, orderEndpoint, params, limitOrder, &resp)
}
// OpenOrders Current open orders. Get all open orders on a symbol.
// Careful when accessing this with no symbol: The number of requests counted against the rate limiter
// is significantly higher
func (b *Binance) OpenOrders(pair *currency.Pair) ([]QueryOrderData, error) {
var resp []QueryOrderData
params := url.Values{}
var symbol string
if pair != nil {
var err error
symbol, err = b.FormatSymbol(*pair, asset.Spot)
if err != nil {
return resp, err
}
}
if symbol != "" {
params.Set("symbol", symbol)
}
if err := b.SendAuthHTTPRequest(exchange.RestSpotSupplementary, http.MethodGet, openOrders, params, openOrdersLimit(symbol), &resp); err != nil {
return resp, err
}
return resp, nil
}
// AllOrders Get all account orders; active, canceled, or filled.
// orderId optional param
// limit optional param, default 500; max 500
func (b *Binance) AllOrders(symbol currency.Pair, orderID, limit string) ([]QueryOrderData, error) {
var resp []QueryOrderData
params := url.Values{}
symbolValue, err := b.FormatSymbol(symbol, asset.Spot)
if err != nil {
return resp, err
}
params.Set("symbol", symbolValue)
if orderID != "" {
params.Set("orderId", orderID)
}
if limit != "" {
params.Set("limit", limit)
}
if err := b.SendAuthHTTPRequest(exchange.RestSpotSupplementary, http.MethodGet, allOrders, params, limitOrdersAll, &resp); err != nil {
return resp, err
}
return resp, nil
}
// QueryOrder returns information on a past order
func (b *Binance) QueryOrder(symbol currency.Pair, origClientOrderID string, orderID int64) (QueryOrderData, error) {
var resp QueryOrderData
params := url.Values{}
symbolValue, err := b.FormatSymbol(symbol, asset.Spot)
if err != nil {
return resp, err
}
params.Set("symbol", symbolValue)
if origClientOrderID != "" {
params.Set("origClientOrderId", origClientOrderID)
}
if orderID != 0 {
params.Set("orderId", strconv.FormatInt(orderID, 10))
}
if err := b.SendAuthHTTPRequest(exchange.RestSpotSupplementary, http.MethodGet, orderEndpoint, params, limitOrder, &resp); err != nil {
return resp, err
}
if resp.Code != 0 {
return resp, errors.New(resp.Msg)
}
return resp, nil
}
// GetAccount returns binance user accounts
func (b *Binance) GetAccount() (*Account, error) {
type response struct {
Response
Account
}
var resp response
params := url.Values{}
if err := b.SendAuthHTTPRequest(exchange.RestSpotSupplementary, http.MethodGet, accountInfo, params, request.Unset, &resp); err != nil {
return &resp.Account, err
}
if resp.Code != 0 {
return &resp.Account, errors.New(resp.Msg)
}
return &resp.Account, nil
}
// SendHTTPRequest sends an unauthenticated request
func (b *Binance) SendHTTPRequest(ePath exchange.URL, path string, f request.EndpointLimit, result interface{}) error {
endpointPath, err := b.API.Endpoints.GetURL(ePath)
if err != nil {
return err
}
return b.SendPayload(context.Background(), &request.Item{
Method: http.MethodGet,
Path: endpointPath + path,
Result: result,
Verbose: b.Verbose,
HTTPDebugging: b.HTTPDebugging,
HTTPRecording: b.HTTPRecording,
Endpoint: f})
}
// SendAuthHTTPRequest sends an authenticated HTTP request
func (b *Binance) SendAuthHTTPRequest(ePath exchange.URL, method, path string, params url.Values, f request.EndpointLimit, result interface{}) error {
if !b.AllowAuthenticatedRequest() {
return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, b.Name)
}
endpointPath, err := b.API.Endpoints.GetURL(ePath)
if err != nil {
return err
}
path = endpointPath + path
if params == nil {
params = url.Values{}
}
recvWindow := 5 * time.Second
params.Set("recvWindow", strconv.FormatInt(convert.RecvWindow(recvWindow), 10))
params.Set("timestamp", strconv.FormatInt(time.Now().Unix()*1000, 10))
signature := params.Encode()
hmacSigned := crypto.GetHMAC(crypto.HashSHA256, []byte(signature), []byte(b.API.Credentials.Secret))
hmacSignedStr := crypto.HexEncodeToString(hmacSigned)
headers := make(map[string]string)
headers["X-MBX-APIKEY"] = b.API.Credentials.Key
if b.Verbose {
log.Debugf(log.ExchangeSys, "sent path: %s", path)
}
path = common.EncodeURLValues(path, params)
path += "&signature=" + hmacSignedStr
interim := json.RawMessage{}
errCap := struct {
Success bool `json:"success"`
Message string `json:"msg"`
Code int64 `json:"code"`
}{}
ctx, cancel := context.WithTimeout(context.Background(), recvWindow)
defer cancel()
err = b.SendPayload(ctx, &request.Item{
Method: method,
Path: path,
Headers: headers,
Body: bytes.NewBuffer(nil),
Result: &interim,
AuthRequest: true,
Verbose: b.Verbose,
HTTPDebugging: b.HTTPDebugging,
HTTPRecording: b.HTTPRecording,
Endpoint: f})
if err != nil {
return err
}
if err := json.Unmarshal(interim, &errCap); err == nil {
if !errCap.Success && errCap.Message != "" && errCap.Code != 200 {
return errors.New(errCap.Message)
}
}
return json.Unmarshal(interim, result)
}
// CheckLimit checks value against a variable list
func (b *Binance) CheckLimit(limit int) error {
for x := range b.validLimits {
if b.validLimits[x] == limit {
return nil
}
}
return errors.New("incorrect limit values - valid values are 5, 10, 20, 50, 100, 500, 1000")
}
// SetValues sets the default valid values
func (b *Binance) SetValues() {
b.validLimits = []int{5, 10, 20, 50, 100, 500, 1000, 5000}
}
// GetFee returns an estimate of fee based on type of transaction
func (b *Binance) GetFee(feeBuilder *exchange.FeeBuilder) (float64, error) {
var fee float64
switch feeBuilder.FeeType {
case exchange.CryptocurrencyTradeFee:
multiplier, err := b.getMultiplier(feeBuilder.IsMaker)
if err != nil {
return 0, err
}
fee = calculateTradingFee(feeBuilder.PurchasePrice, feeBuilder.Amount, multiplier)
case exchange.CryptocurrencyWithdrawalFee:
fee = getCryptocurrencyWithdrawalFee(feeBuilder.Pair.Base)
case exchange.OfflineTradeFee:
fee = getOfflineTradeFee(feeBuilder.PurchasePrice, feeBuilder.Amount)
}
if fee < 0 {
fee = 0
}
return fee, nil
}
// getOfflineTradeFee calculates the worst case-scenario trading fee
func getOfflineTradeFee(price, amount float64) float64 {
return 0.002 * price * amount
}
// getMultiplier retrieves account based taker/maker fees
func (b *Binance) getMultiplier(isMaker bool) (float64, error) {
var multiplier float64
account, err := b.GetAccount()
if err != nil {
return 0, err
}
if isMaker {
multiplier = float64(account.MakerCommission)
} else {
multiplier = float64(account.TakerCommission)
}
return multiplier, nil
}
// calculateTradingFee returns the fee for trading any currency on Bittrex
func calculateTradingFee(purchasePrice, amount, multiplier float64) float64 {
return (multiplier / 100) * purchasePrice * amount
}
// getCryptocurrencyWithdrawalFee returns the fee for withdrawing from the exchange
func getCryptocurrencyWithdrawalFee(c currency.Code) float64 {
return WithdrawalFees[c]
}
// WithdrawCrypto sends cryptocurrency to the address of your choosing
func (b *Binance) WithdrawCrypto(asset, address, addressTag, name, amount string) (string, error) {
var resp WithdrawResponse
params := url.Values{}
params.Set("asset", asset)
params.Set("address", address)
params.Set("amount", amount)
if len(name) > 0 {
params.Set("name", name)
}
if len(addressTag) > 0 {
params.Set("addressTag", addressTag)
}
if err := b.SendAuthHTTPRequest(exchange.RestSpotSupplementary, http.MethodPost, withdrawEndpoint, params, request.Unset, &resp); err != nil {
return "", err
}
if !resp.Success {
return resp.ID, errors.New(resp.Msg)
}
return resp.ID, nil
}
// WithdrawStatus gets the status of recent withdrawals
// status `param` used as string to prevent default value 0 (for int) interpreting as EmailSent status
func (b *Binance) WithdrawStatus(c currency.Code, status string, startTime, endTime int64) ([]WithdrawStatusResponse, error) {
var response struct {
Success bool `json:"success"`
WithdrawList []WithdrawStatusResponse `json:"withdrawList"`
}
params := url.Values{}
params.Set("asset", c.String())
if status != "" {
i, err := strconv.Atoi(status)
if err != nil {
return response.WithdrawList, fmt.Errorf("wrong param (status): %s. Error: %v", status, err)
}
switch i {
case EmailSent, Cancelled, AwaitingApproval, Rejected, Processing, Failure, Completed:
default:
return response.WithdrawList, fmt.Errorf("wrong param (status): %s", status)
}
params.Set("status", status)
}
if startTime > 0 {
params.Set("startTime", strconv.FormatInt(startTime, 10))
}
if endTime > 0 {
params.Set("endTime", strconv.FormatInt(endTime, 10))
}
if err := b.SendAuthHTTPRequest(exchange.RestSpotSupplementary, http.MethodGet, withdrawalHistory, params, request.Unset, &response); err != nil {
return response.WithdrawList, err
}
return response.WithdrawList, nil
}
// GetDepositAddressForCurrency retrieves the wallet address for a given currency
func (b *Binance) GetDepositAddressForCurrency(currency string) (string, error) {
resp := struct {
Address string `json:"address"`
Success bool `json:"success"`
AddressTag string `json:"addressTag"`
}{}
params := url.Values{}
params.Set("asset", currency)
params.Set("status", "true")
return resp.Address,
b.SendAuthHTTPRequest(exchange.RestSpotSupplementary, http.MethodGet, depositAddress, params, request.Unset, &resp)
}
// GetWsAuthStreamKey will retrieve a key to use for authorised WS streaming
func (b *Binance) GetWsAuthStreamKey() (string, error) {
endpointPath, err := b.API.Endpoints.GetURL(exchange.RestSpotSupplementary)
if err != nil {
return "", err
}
var resp UserAccountStream
path := endpointPath + userAccountStream
headers := make(map[string]string)
headers["X-MBX-APIKEY"] = b.API.Credentials.Key
err = b.SendPayload(context.Background(), &request.Item{
Method: http.MethodPost,
Path: path,
Headers: headers,
Body: bytes.NewBuffer(nil),
Result: &resp,
AuthRequest: true,
Verbose: b.Verbose,
HTTPDebugging: b.HTTPDebugging,
HTTPRecording: b.HTTPRecording,
})
if err != nil {
return "", err
}
return resp.ListenKey, nil
}
// MaintainWsAuthStreamKey will keep the key alive
func (b *Binance) MaintainWsAuthStreamKey() error {
endpointPath, err := b.API.Endpoints.GetURL(exchange.RestSpotSupplementary)
if err != nil {
return err
}
if listenKey == "" {
listenKey, err = b.GetWsAuthStreamKey()
return err
}
path := endpointPath + userAccountStream
params := url.Values{}
params.Set("listenKey", listenKey)
path = common.EncodeURLValues(path, params)
headers := make(map[string]string)
headers["X-MBX-APIKEY"] = b.API.Credentials.Key
return b.SendPayload(context.Background(), &request.Item{
Method: http.MethodPut,
Path: path,
Headers: headers,
Body: bytes.NewBuffer(nil),
AuthRequest: true,
Verbose: b.Verbose,
HTTPDebugging: b.HTTPDebugging,
HTTPRecording: b.HTTPRecording,
})
}