mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-13 15:09:42 +00:00
* 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
1044 lines
33 KiB
Go
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
|
|
}
|