Files
gocryptotrader/exchanges/binance/binance.go
Scott 197ef2df21 Feature: Data history manager engine subsystem (#693)
* Adds lovely initial concept for historical data doer

* Adds ability to save tasks. Adds config. Adds startStop to engine

* Has a database microservice without use of globals! Further infrastructure design. Adds readme

* Commentary to help design

* Adds migrations for database

* readme and adds database models

* Some modelling that doesn't work end of day

* Completes datahistoryjob sql.Begins datahistoryjobresult

* Adds datahistoryjob functions to retreive job results. Adapts subsystem

* Adds process for upserting jobs and job results to the database

* Broken end of day weird sqlboiler crap

* Fixes issue with SQL generation.

* RPC generation and addition of basic upsert command

* Renames types

* Adds rpc functions

* quick commit before context swithc. Exchanges aren't being populated

* Begin the tests!

* complete sql tests. stop failed jobs. CLI command creation

* Defines rpc commands

* Fleshes out RPC implementation

* Expands testing

* Expands testing, removes double remove

* Adds coverage of data history subsystem, expands errors and nil checks

* Minor logic improvement

* streamlines datahistory test setup

* End of day minor linting

* Lint, convert simplify, rpc expansion, type expansion, readme expansion

* Documentation update

* Renames for consistency

* Completes RPC server commands

* Fixes tests

* Speeds up testing by reducing unnecessary actions. Adds maxjobspercycle config

* Comments for everything

* Adds missing result string. checks interval supported. default start end cli

* Fixes ID problem. Improves binance trade fetch. job ranges are processed

* adds dbservice coverage. adds rpcserver coverage

* docs regen, uses dbcon interface, reverts binance, fixes races, toggle manager

* Speed up tests, remove bad global usage, fix uuid check

* Adds verbose. Updates docs. Fixes postgres

* Minor changes to logging and start stop

* Fixes postgres db tests, fixes postgres column typo

* Fixes old string typo,removes constraint,error parsing for nonreaders

* prevents dhm running when table doesn't exist. Adds prereq documentation

* Adds parallel, rmlines, err fix, comment fix, minor param fixes

* doc regen, common time range check and test updating

* Fixes job validation issues. Updates candle range checker.

* Ensures test cannot fail due to time.Now() shenanigans

* Fixes oopsie, adds documentation and a warn

* Fixes another time test, adjusts copy

* Drastically speeds up data history manager tests via function overrides

* Fixes summary bug and better logs

* Fixes local time test, fixes websocket tests

* removes defaults and comment,updates error messages,sets cli command args

* Fixes FTX trade processing

* Fixes issue where jobs got stuck if data wasn't returned but retrieval was successful

* Improves test speed. Simplifies trade verification SQL. Adds command help

* Fixes the oopsies

* Fixes use of query within transaction. Fixes trade err

* oopsie, not needed

* Adds missing data status. Properly ends job even when data is missing

* errors are more verbose and so have more words to describe them

* Doc regen for new status

* tiny test tinkering

* str := string("Removes .String()").String()

* Merge fixups

* Fixes a data race discovered during github actions

* Allows websocket test to pass consistently

* Fixes merge issue preventing datahistorymanager from starting via config

* Niterinos cmd defaults and explanations

* fixes default oopsie

* Fixes lack of nil protection

* Additional oopsie

* More detailed error for validating job exchange
2021-07-01 16:21:48 +10:00

1044 lines
33 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/order"
"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"
userAccountStream = "/api/v3/userDataStream"
perpExchangeInfo = "/fapi/v1/exchangeInfo"
historicalTrades = "/api/v3/historicalTrades"
// Authenticated endpoints
newOrderTest = "/api/v3/order/test"
orderEndpoint = "/api/v3/order"
openOrders = "/api/v3/openOrders"
allOrders = "/api/v3/allOrders"
accountInfo = "/api/v3/account"
marginAccountInfo = "/sapi/v1/margin/account"
// 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, spotDefaultRate, &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, spotDefaultRate, &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, spotDefaultRate, &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, spotExchangeInfo, &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, spotDefaultRate, &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) {
var resp []HistoricalTrade
params := url.Values{}
params.Set("symbol", symbol)
params.Set("limit", fmt.Sprintf("%d", limit))
// else return most recent trades
if fromID > 0 {
params.Set("fromId", fmt.Sprintf("%d", fromID))
}
path := historicalTrades + "?" + params.Encode()
return resp, b.SendAPIKeyHTTPRequest(exchange.RestSpotSupplementary, path, spotDefaultRate, &resp)
}
// 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, spotDefaultRate, &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, spotDefaultRate, &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, spotDefaultRate, &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, spotDefaultRate, &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, spotDefaultRate, &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{}
rateLimit := spotPriceChangeAllRate
if !symbol.IsEmpty() {
rateLimit = spotDefaultRate
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, rateLimit, &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, spotPriceChangeAllRate, &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{}
rateLimit := spotSymbolPriceAllRate
if !symbol.IsEmpty() {
rateLimit = spotDefaultRate
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, rateLimit, &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{}
rateLimit := spotOrderbookTickerAllRate
if !symbol.IsEmpty() {
rateLimit = spotDefaultRate
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, rateLimit, &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, spotOrderRate, 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, spotOrderRate, &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 p string
var err error
if !pair.IsEmpty() {
p, err = b.FormatSymbol(pair, asset.Spot)
if err != nil {
return nil, err
}
params.Add("symbol", p)
} else {
// extend the receive window when all currencies to prevent "recvwindow" error
params.Set("recvWindow", "10000")
}
if err := b.SendAuthHTTPRequest(exchange.RestSpotSupplementary, http.MethodGet, openOrders, params, openOrdersLimit(p), &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, spotAllOrdersRate, &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, spotOrderQueryRate, &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, spotAccountInformationRate, &resp); err != nil {
return &resp.Account, err
}
if resp.Code != 0 {
return &resp.Account, errors.New(resp.Msg)
}
return &resp.Account, nil
}
// GetMarginAccount returns account information for margin accounts
func (b *Binance) GetMarginAccount() (*MarginAccount, error) {
var resp MarginAccount
params := url.Values{}
if err := b.SendAuthHTTPRequest(exchange.RestSpotSupplementary, http.MethodGet, marginAccountInfo, params, spotAccountInformationRate, &resp); err != nil {
return &resp, err
}
return &resp, 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})
}
// SendAPIKeyHTTPRequest is a special API request where the api key is
// appended to the headers without a secret
func (b *Binance) SendAPIKeyHTTPRequest(ePath exchange.URL, path string, f request.EndpointLimit, result interface{}) error {
endpointPath, err := b.API.Endpoints.GetURL(ePath)
if err != nil {
return err
}
headers := make(map[string]string)
headers["X-MBX-APIKEY"] = b.API.Credentials.Key
return b.SendPayload(context.Background(), &request.Item{
Method: http.MethodGet,
Path: endpointPath + path,
Headers: headers,
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("%s %w", b.Name, exchange.ErrAuthenticatedRequestWithoutCredentialsSet)
}
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
if params.Get("recvWindow") != "" {
// convert recvWindow value into time.Duration
var recvWindowParam int64
recvWindowParam, err = convert.Int64FromString(params.Get("recvWindow"))
if err != nil {
return err
}
recvWindow = time.Duration(recvWindowParam) * time.Millisecond
} else {
params.Set("recvWindow", strconv.FormatInt(convert.RecvWindow(recvWindow), 10))
}
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, spotDefaultRate, &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, spotDefaultRate, &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")
params.Set("recvWindow", "10000")
return resp.Address,
b.SendAuthHTTPRequest(exchange.RestSpotSupplementary, http.MethodGet, depositAddress, params, spotDefaultRate, &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,
})
}
// FetchSpotExchangeLimits fetches spot order execution limits
func (b *Binance) FetchSpotExchangeLimits() ([]order.MinMaxLevel, error) {
var limits []order.MinMaxLevel
spot, err := b.GetExchangeInfo()
if err != nil {
return nil, err
}
for x := range spot.Symbols {
var cp currency.Pair
cp, err = currency.NewPairFromStrings(spot.Symbols[x].BaseAsset,
spot.Symbols[x].QuoteAsset)
if err != nil {
return nil, err
}
var assets []asset.Item
for y := range spot.Symbols[x].Permissions {
switch spot.Symbols[x].Permissions[y] {
case "SPOT":
assets = append(assets, asset.Spot)
case "MARGIN":
assets = append(assets, asset.Margin)
case "LEVERAGED": // leveraged tokens not available for spot trading
default:
return nil, fmt.Errorf("unhandled asset type for exchange limits loading %s",
spot.Symbols[x].Permissions[y])
}
}
for z := range assets {
if len(spot.Symbols[x].Filters) < 8 {
continue
}
limits = append(limits, order.MinMaxLevel{
Pair: cp,
Asset: assets[z],
MinPrice: spot.Symbols[x].Filters[0].MinPrice,
MaxPrice: spot.Symbols[x].Filters[0].MaxPrice,
StepPrice: spot.Symbols[x].Filters[0].TickSize,
MultiplierUp: spot.Symbols[x].Filters[1].MultiplierUp,
MultiplierDown: spot.Symbols[x].Filters[1].MultiplierDown,
AveragePriceMinutes: spot.Symbols[x].Filters[1].AvgPriceMinutes,
MaxAmount: spot.Symbols[x].Filters[2].MaxQty,
MinAmount: spot.Symbols[x].Filters[2].MinQty,
StepAmount: spot.Symbols[x].Filters[2].StepSize,
MinNotional: spot.Symbols[x].Filters[3].MinNotional,
MaxIcebergParts: spot.Symbols[x].Filters[4].Limit,
MarketMinQty: spot.Symbols[x].Filters[5].MinQty,
MarketMaxQty: spot.Symbols[x].Filters[5].MaxQty,
MarketStepSize: spot.Symbols[x].Filters[5].StepSize,
MaxTotalOrders: spot.Symbols[x].Filters[6].MaxNumOrders,
MaxAlgoOrders: spot.Symbols[x].Filters[7].MaxNumAlgoOrders,
})
}
}
return limits, nil
}