bbgo_origin/pkg/exchange/binance/exchange.go

1490 lines
42 KiB
Go
Raw Normal View History

2020-07-11 05:02:53 +00:00
package binance
import (
"context"
2020-07-11 07:27:26 +00:00
"fmt"
2020-12-21 09:48:30 +00:00
"os"
"strconv"
"strings"
"sync"
2020-07-11 07:19:36 +00:00
"time"
"github.com/adshao/go-binance/v2"
2021-12-23 05:15:27 +00:00
"github.com/adshao/go-binance/v2/futures"
"github.com/spf13/viper"
2021-12-23 05:15:27 +00:00
2021-12-14 11:09:44 +00:00
"go.uber.org/multierr"
"golang.org/x/time/rate"
"github.com/google/uuid"
"github.com/pkg/errors"
2020-08-13 02:11:27 +00:00
"github.com/sirupsen/logrus"
"github.com/c9s/bbgo/pkg/exchange/binance/binanceapi"
2020-11-10 06:19:33 +00:00
"github.com/c9s/bbgo/pkg/fixedpoint"
2020-10-11 08:46:15 +00:00
"github.com/c9s/bbgo/pkg/types"
"github.com/c9s/bbgo/pkg/util"
2020-07-11 05:02:53 +00:00
)
2021-05-25 18:15:47 +00:00
const BNB = "BNB"
2021-12-30 15:46:43 +00:00
const BinanceUSBaseURL = "https://api.binance.us"
2022-02-25 03:30:40 +00:00
const BinanceTestBaseURL = "https://testnet.binance.vision"
2021-12-30 15:46:43 +00:00
const BinanceUSWebSocketURL = "wss://stream.binance.us:9443"
const WebSocketURL = "wss://stream.binance.com:9443"
2022-02-25 03:30:40 +00:00
const WebSocketTestURL = "wss://testnet.binance.vision"
const FutureTestBaseURL = "https://testnet.binancefuture.com"
const FuturesWebSocketURL = "wss://fstream.binance.com"
2022-02-25 03:30:40 +00:00
const FuturesWebSocketTestURL = "wss://stream.binancefuture.com"
2021-12-30 15:46:43 +00:00
2022-09-12 07:03:01 +00:00
// orderLimiter - the default order limiter apply 5 requests per second and a 2 initial bucket
// this includes SubmitOrder, CancelOrder and QueryClosedOrders
//
// Limit defines the maximum frequency of some events. Limit is represented as number of events per second. A zero Limit allows no events.
var orderLimiter = rate.NewLimiter(5, 2)
2022-09-12 07:03:01 +00:00
var queryTradeLimiter = rate.NewLimiter(1, 2)
2021-11-04 17:25:16 +00:00
2020-08-11 00:36:36 +00:00
var log = logrus.WithFields(logrus.Fields{
"exchange": "binance",
})
func init() {
_ = types.Exchange(&Exchange{})
2021-01-19 18:09:12 +00:00
_ = types.MarginExchange(&Exchange{})
_ = types.FuturesExchange(&Exchange{})
2022-09-12 07:03:01 +00:00
if n, ok := util.GetEnvVarInt("BINANCE_ORDER_RATE_LIMITER"); ok {
orderLimiter = rate.NewLimiter(rate.Every(time.Duration(n)*time.Minute), 2)
2022-09-12 07:03:01 +00:00
}
if n, ok := util.GetEnvVarInt("BINANCE_QUERY_TRADES_RATE_LIMITER"); ok {
queryTradeLimiter = rate.NewLimiter(rate.Every(time.Duration(n)*time.Minute), 2)
2022-09-12 07:03:01 +00:00
}
2020-12-21 09:48:30 +00:00
}
2021-12-30 15:46:43 +00:00
func isBinanceUs() bool {
v, err := strconv.ParseBool(os.Getenv("BINANCE_US"))
return err == nil && v
}
2022-02-25 03:30:40 +00:00
func paperTrade() bool {
v, ok := util.GetEnvVarBool("PAPER_TRADE")
return ok && v
2022-02-25 03:30:40 +00:00
}
2020-07-11 05:02:53 +00:00
type Exchange struct {
2021-01-19 18:09:12 +00:00
types.MarginSettings
types.FuturesSettings
2020-12-02 14:19:31 +00:00
2022-04-26 08:43:40 +00:00
key, secret string
// client is used for spot & margin
2022-04-26 08:43:40 +00:00
client *binance.Client
// futuresClient is used for usdt-m futures
futuresClient *futures.Client // USDT-M Futures
// deliveryClient *delivery.Client // Coin-M Futures
// client2 is a newer version of the binance api client implemented by ourselves.
client2 *binanceapi.RestClient
futuresClient2 *binanceapi.FuturesRestClient
2020-07-11 05:02:53 +00:00
}
2022-07-26 08:22:57 +00:00
var timeSetterOnce sync.Once
func New(key, secret string) *Exchange {
2020-07-11 05:08:50 +00:00
var client = binance.NewClient(key, secret)
client.HTTPClient = binanceapi.DefaultHttpClient
client.Debug = viper.GetBool("debug-binance-client")
var futuresClient = binance.NewFuturesClient(key, secret)
futuresClient.HTTPClient = binanceapi.DefaultHttpClient
futuresClient.Debug = viper.GetBool("debug-binance-futures-client")
2021-12-30 15:46:43 +00:00
if isBinanceUs() {
client.BaseURL = BinanceUSBaseURL
}
2022-02-25 03:30:40 +00:00
if paperTrade() {
client.BaseURL = BinanceTestBaseURL
futuresClient.BaseURL = FutureTestBaseURL
}
client2 := binanceapi.NewClient(client.BaseURL)
futuresClient2 := binanceapi.NewFuturesRestClient(futuresClient.BaseURL)
ex := &Exchange{
key: key,
secret: secret,
client: client,
futuresClient: futuresClient,
client2: client2,
futuresClient2: futuresClient2,
}
if len(key) > 0 && len(secret) > 0 {
client2.Auth(key, secret)
2023-03-26 07:03:23 +00:00
futuresClient2.Auth(key, secret)
ctx := context.Background()
2022-07-26 08:22:57 +00:00
go timeSetterOnce.Do(func() {
ex.setServerTimeOffset(ctx)
ticker := time.NewTicker(time.Hour)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
ex.setServerTimeOffset(ctx)
}
}
})
}
return ex
}
func (e *Exchange) setServerTimeOffset(ctx context.Context) {
_, err := e.client.NewSetServerTimeService().Do(ctx)
if err != nil {
log.WithError(err).Error("can not set server time")
}
_, err = e.futuresClient.NewSetServerTimeService().Do(ctx)
if err != nil {
log.WithError(err).Error("can not set server time")
}
if err = e.client2.SetTimeOffsetFromServer(ctx); err != nil {
log.WithError(err).Error("can not set server time")
2020-07-11 05:08:50 +00:00
}
}
func (e *Exchange) Name() types.ExchangeName {
return types.ExchangeBinance
}
func (e *Exchange) QueryTicker(ctx context.Context, symbol string) (*types.Ticker, error) {
if e.IsFutures {
req := e.futuresClient.NewListPriceChangeStatsService()
req.Symbol(strings.ToUpper(symbol))
stats, err := req.Do(ctx)
if err != nil {
return nil, err
}
return toGlobalFuturesTicker(stats[0])
}
req := e.client.NewListPriceChangeStatsService()
req.Symbol(strings.ToUpper(symbol))
stats, err := req.Do(ctx)
if err != nil {
return nil, err
}
2021-12-08 16:05:36 +00:00
return toGlobalTicker(stats[0])
}
2021-02-06 17:35:23 +00:00
func (e *Exchange) QueryTickers(ctx context.Context, symbol ...string) (map[string]types.Ticker, error) {
var tickers = make(map[string]types.Ticker)
2021-02-06 17:35:23 +00:00
if len(symbol) == 1 {
ticker, err := e.QueryTicker(ctx, symbol[0])
if err != nil {
return nil, err
}
tickers[strings.ToUpper(symbol[0])] = *ticker
return tickers, nil
2021-02-06 17:35:23 +00:00
}
2021-02-07 21:58:30 +00:00
m := make(map[string]struct{})
exists := struct{}{}
2021-02-06 17:35:23 +00:00
for _, s := range symbol {
2021-02-07 21:58:30 +00:00
m[s] = exists
2021-02-06 17:35:23 +00:00
}
if e.IsFutures {
var req = e.futuresClient.NewListPriceChangeStatsService()
changeStats, err := req.Do(ctx)
if err != nil {
return nil, err
}
for _, stats := range changeStats {
if _, ok := m[stats.Symbol]; len(symbol) != 0 && !ok {
continue
}
tick := types.Ticker{
Volume: fixedpoint.MustNewFromString(stats.Volume),
Last: fixedpoint.MustNewFromString(stats.LastPrice),
Open: fixedpoint.MustNewFromString(stats.OpenPrice),
High: fixedpoint.MustNewFromString(stats.HighPrice),
Low: fixedpoint.MustNewFromString(stats.LowPrice),
Buy: fixedpoint.MustNewFromString(stats.LastPrice),
Sell: fixedpoint.MustNewFromString(stats.LastPrice),
Time: time.Unix(0, stats.CloseTime*int64(time.Millisecond)),
}
tickers[stats.Symbol] = tick
}
return tickers, nil
}
var req = e.client.NewListPriceChangeStatsService()
changeStats, err := req.Do(ctx)
if err != nil {
return nil, err
}
2021-02-07 21:58:30 +00:00
for _, stats := range changeStats {
if _, ok := m[stats.Symbol]; len(symbol) != 0 && !ok {
2021-02-06 17:35:23 +00:00
continue
}
tick := types.Ticker{
Volume: fixedpoint.MustNewFromString(stats.Volume),
Last: fixedpoint.MustNewFromString(stats.LastPrice),
Open: fixedpoint.MustNewFromString(stats.OpenPrice),
High: fixedpoint.MustNewFromString(stats.HighPrice),
Low: fixedpoint.MustNewFromString(stats.LowPrice),
Buy: fixedpoint.MustNewFromString(stats.BidPrice),
Sell: fixedpoint.MustNewFromString(stats.AskPrice),
2021-02-07 21:58:30 +00:00
Time: time.Unix(0, stats.CloseTime*int64(time.Millisecond)),
2021-02-06 17:35:23 +00:00
}
tickers[stats.Symbol] = tick
2021-02-06 17:35:23 +00:00
}
return tickers, nil
2021-02-04 21:48:35 +00:00
}
2020-10-14 02:16:59 +00:00
func (e *Exchange) QueryMarkets(ctx context.Context) (types.MarketMap, error) {
2022-01-03 19:30:36 +00:00
if e.IsFutures {
exchangeInfo, err := e.futuresClient.NewExchangeInfoService().Do(ctx)
if err != nil {
return nil, err
}
markets := types.MarketMap{}
for _, symbol := range exchangeInfo.Symbols {
markets[symbol.Symbol] = toGlobalFuturesMarket(symbol)
}
return markets, nil
}
exchangeInfo, err := e.client.NewExchangeInfoService().Do(ctx)
2020-10-14 02:16:59 +00:00
if err != nil {
return nil, err
}
markets := types.MarketMap{}
for _, symbol := range exchangeInfo.Symbols {
2021-12-08 16:05:36 +00:00
markets[symbol.Symbol] = toGlobalMarket(symbol)
2020-10-14 02:16:59 +00:00
}
return markets, nil
}
func (e *Exchange) QueryAveragePrice(ctx context.Context, symbol string) (fixedpoint.Value, error) {
resp, err := e.client.NewAveragePriceService().Symbol(symbol).Do(ctx)
2020-07-11 05:02:53 +00:00
if err != nil {
return fixedpoint.Zero, err
2020-07-11 05:02:53 +00:00
}
return fixedpoint.MustNewFromString(resp.Price), nil
2020-07-11 05:02:53 +00:00
}
2020-10-03 12:09:22 +00:00
func (e *Exchange) NewStream() types.Stream {
stream := NewStream(e, e.client, e.futuresClient)
2020-12-21 09:48:30 +00:00
stream.MarginSettings = e.MarginSettings
stream.FuturesSettings = e.FuturesSettings
2020-12-21 09:48:30 +00:00
return stream
}
2020-12-02 14:19:31 +00:00
func (e *Exchange) QueryMarginAssetMaxBorrowable(ctx context.Context, asset string) (amount fixedpoint.Value, err error) {
req := e.client2.NewGetMarginMaxBorrowableRequest()
req.Asset(asset)
if e.IsIsolatedMargin {
req.IsolatedSymbol(e.IsolatedMarginSymbol)
}
resp, err := req.Do(ctx)
if err != nil {
2022-04-22 06:45:03 +00:00
return fixedpoint.Zero, err
}
return resp.Amount, nil
}
2022-04-22 06:07:09 +00:00
func (e *Exchange) RepayMarginAsset(ctx context.Context, asset string, amount fixedpoint.Value) error {
req := e.client.NewMarginRepayService()
2022-04-22 06:07:09 +00:00
req.Asset(asset)
req.Amount(amount.String())
if e.IsIsolatedMargin {
req.Symbol(e.IsolatedMarginSymbol)
2022-04-22 06:07:09 +00:00
}
2022-04-23 07:00:04 +00:00
log.Infof("repaying margin asset %s amount %f", asset, amount.Float64())
2022-04-22 06:07:09 +00:00
resp, err := req.Do(ctx)
2022-04-26 08:51:41 +00:00
if err != nil {
return err
}
2022-04-22 06:07:09 +00:00
log.Debugf("margin repayed %f %s, transaction id = %d", amount.Float64(), asset, resp.TranID)
return err
2022-04-22 06:05:33 +00:00
}
2022-04-22 06:07:09 +00:00
func (e *Exchange) BorrowMarginAsset(ctx context.Context, asset string, amount fixedpoint.Value) error {
req := e.client.NewMarginLoanService()
2022-04-22 06:05:33 +00:00
req.Asset(asset)
req.Amount(amount.String())
if e.IsIsolatedMargin {
req.Symbol(e.IsolatedMarginSymbol)
2022-04-22 06:05:33 +00:00
}
2022-04-23 07:00:04 +00:00
log.Infof("borrowing margin asset %s amount %f", asset, amount.Float64())
2022-04-22 06:05:33 +00:00
resp, err := req.Do(ctx)
2022-04-26 08:43:40 +00:00
if err != nil {
return err
}
2022-04-22 06:05:33 +00:00
log.Debugf("margin borrowed %f %s, transaction id = %d", amount.Float64(), asset, resp.TranID)
return err
}
func (e *Exchange) QueryMarginBorrowHistory(ctx context.Context, asset string) error {
req := e.client.NewListMarginLoansService()
req.Asset(asset)
history, err := req.Do(ctx)
if err != nil {
return err
}
_ = history
return nil
}
// TransferMarginAccountAsset transfers the asset into/out from the margin account
//
// types.TransferIn => Spot to Margin
// types.TransferOut => Margin to Spot
//
// to call this method, you must set the IsMargin = true
func (e *Exchange) TransferMarginAccountAsset(ctx context.Context, asset string, amount fixedpoint.Value, io types.TransferDirection) error {
if e.IsIsolatedMargin {
return e.transferIsolatedMarginAccountAsset(ctx, asset, amount, io)
2023-08-06 07:27:00 +00:00
}
return e.transferCrossMarginAccountAsset(ctx, asset, amount, io)
2023-08-06 07:27:00 +00:00
}
func (e *Exchange) transferIsolatedMarginAccountAsset(ctx context.Context, asset string, amount fixedpoint.Value, io types.TransferDirection) error {
req := e.client2.NewTransferIsolatedMarginAccountRequest()
req.Symbol(e.IsolatedMarginSymbol)
switch io {
case types.TransferIn:
req.TransFrom(binanceapi.AccountTypeSpot)
req.TransTo(binanceapi.AccountTypeIsolatedMargin)
case types.TransferOut:
req.TransFrom(binanceapi.AccountTypeIsolatedMargin)
req.TransTo(binanceapi.AccountTypeSpot)
}
req.Asset(asset)
req.Amount(amount.String())
resp, err := req.Do(ctx)
return logResponse(resp, err, req)
}
2022-04-22 06:05:33 +00:00
// transferCrossMarginAccountAsset transfer asset to the cross margin account or to the main account
func (e *Exchange) transferCrossMarginAccountAsset(ctx context.Context, asset string, amount fixedpoint.Value, io types.TransferDirection) error {
req := e.client2.NewTransferCrossMarginAccountRequest()
req.Asset(asset)
req.Amount(amount.String())
if io == types.TransferIn {
req.TransferType(int(binance.MarginTransferTypeToMargin))
} else if io == types.TransferOut {
req.TransferType(int(binance.MarginTransferTypeToMain))
} else {
return fmt.Errorf("unexpected transfer direction: %d given", io)
}
resp, err := req.Do(ctx)
return logResponse(resp, err, req)
}
func (e *Exchange) QueryCrossMarginAccount(ctx context.Context) (*types.Account, error) {
marginAccount, err := e.client.NewGetMarginAccountService().Do(ctx)
if err != nil {
return nil, err
}
2022-04-22 06:45:03 +00:00
marginLevel := fixedpoint.MustNewFromString(marginAccount.MarginLevel)
a := &types.Account{
AccountType: types.AccountTypeMargin,
MarginInfo: toGlobalMarginAccountInfo(marginAccount), // In binance GO api, Account define marginAccount info which mantain []*AccountAsset and []*AccountPosition.
2022-04-22 06:45:03 +00:00
MarginLevel: marginLevel,
MarginTolerance: calculateMarginTolerance(marginLevel),
BorrowEnabled: marginAccount.BorrowEnabled,
TransferEnabled: marginAccount.TransferEnabled,
}
// convert cross margin user assets into balances
2022-04-13 07:38:13 +00:00
balances := types.BalanceMap{}
for _, userAsset := range marginAccount.UserAssets {
2022-04-13 07:38:13 +00:00
balances[userAsset.Asset] = types.Balance{
Currency: userAsset.Asset,
Available: fixedpoint.MustNewFromString(userAsset.Free),
Locked: fixedpoint.MustNewFromString(userAsset.Locked),
Interest: fixedpoint.MustNewFromString(userAsset.Interest),
Borrowed: fixedpoint.MustNewFromString(userAsset.Borrowed),
NetAsset: fixedpoint.MustNewFromString(userAsset.NetAsset),
2022-04-13 07:38:13 +00:00
}
}
a.UpdateBalances(balances)
return a, nil
2020-12-21 09:48:30 +00:00
}
func (e *Exchange) QueryIsolatedMarginAccount(ctx context.Context) (*types.Account, error) {
req := e.client.NewGetIsolatedMarginAccountService()
req.Symbols(e.IsolatedMarginSymbol)
2020-12-02 14:19:31 +00:00
marginAccount, err := req.Do(ctx)
if err != nil {
return nil, err
}
a := &types.Account{
2022-04-22 06:45:45 +00:00
AccountType: types.AccountTypeIsolatedMargin,
IsolatedMarginInfo: toGlobalIsolatedMarginAccountInfo(marginAccount), // In binance GO api, Account define marginAccount info which mantain []*AccountAsset and []*AccountPosition.
}
2022-06-03 16:39:24 +00:00
if len(marginAccount.Assets) == 0 {
return nil, fmt.Errorf("empty margin account assets, please check your isolatedMarginSymbol is correctly set: %+v", marginAccount)
}
// for isolated margin account, we will only have one asset in the Assets array.
if len(marginAccount.Assets) > 1 {
return nil, fmt.Errorf("unexpected number of user assets returned, got %d user assets", len(marginAccount.Assets))
}
2022-04-13 07:38:13 +00:00
userAsset := marginAccount.Assets[0]
2022-04-22 06:45:03 +00:00
marginLevel := fixedpoint.MustNewFromString(userAsset.MarginLevel)
a.MarginLevel = marginLevel
a.MarginTolerance = calculateMarginTolerance(marginLevel)
a.MarginRatio = fixedpoint.MustNewFromString(userAsset.MarginRatio)
2022-04-21 08:23:06 +00:00
a.BorrowEnabled = userAsset.BaseAsset.BorrowEnabled || userAsset.QuoteAsset.BorrowEnabled
a.LiquidationPrice = fixedpoint.MustNewFromString(userAsset.LiquidatePrice)
a.LiquidationRate = fixedpoint.MustNewFromString(userAsset.LiquidateRate)
// Convert user assets into balances
balances := types.BalanceMap{}
balances[userAsset.BaseAsset.Asset] = types.Balance{
Currency: userAsset.BaseAsset.Asset,
Available: fixedpoint.MustNewFromString(userAsset.BaseAsset.Free),
Locked: fixedpoint.MustNewFromString(userAsset.BaseAsset.Locked),
Interest: fixedpoint.MustNewFromString(userAsset.BaseAsset.Interest),
Borrowed: fixedpoint.MustNewFromString(userAsset.BaseAsset.Borrowed),
NetAsset: fixedpoint.MustNewFromString(userAsset.BaseAsset.NetAsset),
2022-04-13 07:38:13 +00:00
}
balances[userAsset.QuoteAsset.Asset] = types.Balance{
Currency: userAsset.QuoteAsset.Asset,
Available: fixedpoint.MustNewFromString(userAsset.QuoteAsset.Free),
Locked: fixedpoint.MustNewFromString(userAsset.QuoteAsset.Locked),
Interest: fixedpoint.MustNewFromString(userAsset.QuoteAsset.Interest),
Borrowed: fixedpoint.MustNewFromString(userAsset.QuoteAsset.Borrowed),
NetAsset: fixedpoint.MustNewFromString(userAsset.QuoteAsset.NetAsset),
}
2022-04-13 07:38:13 +00:00
a.UpdateBalances(balances)
return a, nil
2020-07-11 05:02:53 +00:00
}
func (e *Exchange) Withdraw(ctx context.Context, asset string, amount fixedpoint.Value, address string, options *types.WithdrawalOptions) error {
req := e.client2.NewWithdrawRequest()
2021-09-01 16:21:56 +00:00
req.Coin(asset)
req.Address(address)
req.Amount(fmt.Sprintf("%f", amount.Float64()))
2021-05-26 15:24:05 +00:00
if options != nil {
if options.Network != "" {
req.Network(options.Network)
}
if options.AddressTag != "" {
req.Network(options.AddressTag)
}
}
response, err := req.Do(ctx)
if err != nil {
return err
}
log.Infof("withdrawal request sent, response: %+v", response)
return nil
}
func (e *Exchange) QueryWithdrawHistory(ctx context.Context, asset string, since, until time.Time) (withdraws []types.Withdraw, err error) {
var emptyTime = time.Time{}
if since == emptyTime {
since, err = getLaunchDate()
if err != nil {
return withdraws, err
}
}
// startTime ~ endTime must be in 90 days
historyDayRangeLimit := time.Hour * 24 * 89
if until.Sub(since) >= historyDayRangeLimit {
until = since.Add(historyDayRangeLimit)
}
2020-08-13 02:11:27 +00:00
req := e.client2.NewGetWithdrawHistoryRequest()
if len(asset) > 0 {
req.Coin(asset)
}
2020-10-12 09:15:33 +00:00
records, err := req.
StartTime(since).
EndTime(until).
Limit(1000).
Do(ctx)
2020-08-13 02:11:27 +00:00
if err != nil {
return withdraws, err
}
2020-08-13 02:11:27 +00:00
for _, d := range records {
// time format: 2006-01-02 15:04:05
applyTime, err := time.Parse("2006-01-02 15:04:05", d.ApplyTime)
if err != nil {
return nil, err
2020-08-13 02:11:27 +00:00
}
withdraws = append(withdraws, types.Withdraw{
Exchange: types.ExchangeBinance,
ApplyTime: types.Time(applyTime),
Asset: d.Coin,
Amount: d.Amount,
Address: d.Address,
TransactionID: d.TxID,
TransactionFee: d.TransactionFee,
WithdrawOrderID: d.WithdrawOrderID,
Network: d.Network,
Status: d.Status.String(),
})
2020-08-13 02:11:27 +00:00
}
return withdraws, nil
2020-08-13 02:11:27 +00:00
}
func (e *Exchange) QueryDepositHistory(ctx context.Context, asset string, since, until time.Time) (allDeposits []types.Deposit, err error) {
2023-08-06 12:03:52 +00:00
if since.IsZero() {
since, err = getLaunchDate()
if err != nil {
return nil, err
}
}
2020-10-12 09:15:33 +00:00
// startTime ~ endTime must be in 90 days
historyDayRangeLimit := time.Hour * 24 * 89
if until.Sub(since) >= historyDayRangeLimit {
until = since.Add(historyDayRangeLimit)
}
2020-08-13 02:11:27 +00:00
req := e.client2.NewGetDepositHistoryRequest()
if len(asset) > 0 {
req.Coin(asset)
}
2020-08-13 02:11:27 +00:00
req.StartTime(since).
EndTime(until)
2020-08-13 02:11:27 +00:00
records, err := req.Do(ctx)
if err != nil {
return nil, err
}
2020-08-13 02:11:27 +00:00
for _, d := range records {
// 0(0:pending,6: credited but cannot withdraw, 1:success)
// set the default status
status := types.DepositStatus(fmt.Sprintf("code: %d", d.Status))
// https://www.binance.com/en/support/faq/115003736451
switch d.Status {
case binanceapi.DepositStatusPending:
status = types.DepositPending
case binanceapi.DepositStatusCredited:
status = types.DepositCredited
case binanceapi.DepositStatusSuccess:
status = types.DepositSuccess
2020-08-13 02:11:27 +00:00
}
allDeposits = append(allDeposits, types.Deposit{
Exchange: types.ExchangeBinance,
Time: types.Time(d.InsertTime.Time()),
Asset: d.Coin,
Amount: d.Amount,
Address: d.Address,
AddressTag: d.AddressTag,
TransactionID: d.TxId,
Status: status,
UnlockConfirm: d.UnlockConfirm,
Confirmation: d.ConfirmTimes,
})
2020-08-13 02:11:27 +00:00
}
return allDeposits, nil
}
2020-10-06 09:32:41 +00:00
func (e *Exchange) QueryAccountBalances(ctx context.Context) (types.BalanceMap, error) {
2020-07-13 04:28:40 +00:00
account, err := e.QueryAccount(ctx)
if err != nil {
return nil, err
}
2020-10-18 03:30:37 +00:00
return account.Balances(), nil
2020-07-13 04:28:40 +00:00
}
func (e *Exchange) PlatformFeeCurrency() string {
2021-05-25 18:15:47 +00:00
return BNB
2020-08-03 12:06:33 +00:00
}
2022-01-03 19:30:36 +00:00
func (e *Exchange) QuerySpotAccount(ctx context.Context) (*types.Account, error) {
account, err := e.client.NewGetAccountService().Do(ctx)
2020-07-13 04:28:40 +00:00
if err != nil {
return nil, err
}
var balances = map[string]types.Balance{}
2020-07-15 04:20:44 +00:00
for _, b := range account.Balances {
2020-07-13 04:28:40 +00:00
balances[b.Asset] = types.Balance{
Currency: b.Asset,
Available: fixedpoint.MustNewFromString(b.Free),
Locked: fixedpoint.MustNewFromString(b.Locked),
2020-07-13 04:28:40 +00:00
}
}
2020-10-18 03:30:37 +00:00
a := &types.Account{
AccountType: types.AccountTypeSpot,
CanDeposit: account.CanDeposit, // if can transfer in asset
CanTrade: account.CanTrade, // if can trade
CanWithdraw: account.CanWithdraw, // if can transfer out asset
2020-10-18 03:30:37 +00:00
}
a.UpdateBalances(balances)
return a, nil
2020-07-13 04:28:40 +00:00
}
2022-01-03 19:30:36 +00:00
func (e *Exchange) QueryAccount(ctx context.Context) (*types.Account, error) {
var account *types.Account
var err error
if e.IsFutures {
account, err = e.QueryFuturesAccount(ctx)
} else if e.IsIsolatedMargin {
account, err = e.QueryIsolatedMarginAccount(ctx)
} else if e.IsMargin {
account, err = e.QueryCrossMarginAccount(ctx)
} else {
account, err = e.QuerySpotAccount(ctx)
2022-01-03 19:30:36 +00:00
}
2022-01-14 08:10:55 +00:00
return account, err
2022-01-03 19:30:36 +00:00
}
func (e *Exchange) QueryOpenOrders(ctx context.Context, symbol string) (orders []types.Order, err error) {
2021-01-19 18:09:12 +00:00
if e.IsMargin {
req := e.client.NewListMarginOpenOrdersService().Symbol(symbol)
2021-01-19 18:09:12 +00:00
req.IsIsolated(e.IsIsolatedMargin)
2021-01-18 13:56:58 +00:00
binanceOrders, err := req.Do(ctx)
if err != nil {
return orders, err
}
return toGlobalOrders(binanceOrders, false)
}
2021-12-14 10:01:24 +00:00
if e.IsFutures {
req := e.futuresClient.NewListOpenOrdersService().Symbol(symbol)
binanceOrders, err := req.Do(ctx)
if err != nil {
return orders, err
}
return toGlobalFuturesOrders(binanceOrders, false)
2021-12-14 10:01:24 +00:00
}
binanceOrders, err := e.client.NewListOpenOrdersService().Symbol(symbol).Do(ctx)
if err != nil {
return orders, err
}
return toGlobalOrders(binanceOrders, false)
}
func (e *Exchange) QueryOrderTrades(ctx context.Context, q types.OrderQuery) ([]types.Trade, error) {
orderID, err := strconv.ParseInt(q.OrderID, 10, 64)
if err != nil {
return nil, err
}
2022-08-12 03:54:04 +00:00
if len(q.Symbol) == 0 {
return nil, errors.New("binance: symbol parameter is a mandatory parameter for querying order trades")
}
remoteTrades, err := e.client.NewListTradesService().Symbol(q.Symbol).OrderId(orderID).Do(ctx)
if err != nil {
return nil, err
}
var trades []types.Trade
for _, t := range remoteTrades {
localTrade, err := toGlobalTrade(*t, e.IsMargin)
if err != nil {
2022-08-12 03:54:04 +00:00
log.WithError(err).Errorf("binance: can not convert trade: %+v", t)
continue
}
trades = append(trades, *localTrade)
}
trades = types.SortTradesAscending(trades)
return trades, nil
}
func (e *Exchange) QueryOrder(ctx context.Context, q types.OrderQuery) (*types.Order, error) {
orderID, err := strconv.ParseInt(q.OrderID, 10, 64)
if err != nil {
return nil, err
}
var order *binance.Order
if e.IsMargin {
order, err = e.client.NewGetMarginOrderService().Symbol(q.Symbol).OrderID(orderID).Do(ctx)
} else {
order, err = e.client.NewGetOrderService().Symbol(q.Symbol).OrderID(orderID).Do(ctx)
}
if err != nil {
return nil, err
}
return toGlobalOrder(order, e.IsMargin)
}
func (e *Exchange) QueryClosedOrders(ctx context.Context, symbol string, since, until time.Time, lastOrderID uint64) (orders []types.Order, err error) {
2022-01-23 08:19:13 +00:00
// we can only query orders within 24 hours
// if the until-since is more than 24 hours, we should reset the until to:
// new until = since + 24 hours - 1 millisecond
/*
if until.Sub(since) >= 24*time.Hour {
until = since.Add(24*time.Hour - time.Millisecond)
}
*/
2020-11-05 00:33:57 +00:00
2023-07-25 07:09:57 +00:00
if err = orderLimiter.Wait(ctx); err != nil {
2022-01-10 08:33:19 +00:00
log.WithError(err).Errorf("order rate limiter wait error")
2023-07-25 07:09:57 +00:00
return nil, err
2022-01-10 08:33:19 +00:00
}
2021-01-19 18:09:12 +00:00
log.Infof("querying closed orders %s from %s <=> %s ...", symbol, since, until)
2020-11-05 00:33:57 +00:00
2021-01-19 18:09:12 +00:00
if e.IsMargin {
req := e.client.NewListMarginOrdersService().Symbol(symbol)
2021-01-19 18:09:12 +00:00
req.IsIsolated(e.IsIsolatedMargin)
2021-01-18 13:56:58 +00:00
if lastOrderID > 0 {
req.OrderID(int64(lastOrderID))
} else {
2022-04-11 07:39:03 +00:00
req.StartTime(since.UnixNano() / int64(time.Millisecond))
if until.Sub(since) < 24*time.Hour {
req.EndTime(until.UnixNano() / int64(time.Millisecond))
}
2021-01-18 13:56:58 +00:00
}
binanceOrders, err := req.Do(ctx)
if err != nil {
return orders, err
}
return toGlobalOrders(binanceOrders, e.IsMargin)
2021-01-18 13:56:58 +00:00
}
2021-12-14 10:01:24 +00:00
if e.IsFutures {
return e.queryFuturesClosedOrders(ctx, symbol, since, until, lastOrderID)
2021-12-14 10:01:24 +00:00
}
2022-01-23 08:19:13 +00:00
// If orderId is set, it will get orders >= that orderId. Otherwise most recent orders are returned.
// For some historical orders cummulativeQuoteQty will be < 0, meaning the data is not available at this time.
// If startTime and/or endTime provided, orderId is not required.
req := e.client.NewListOrdersService().
Symbol(symbol)
2020-11-05 00:33:57 +00:00
if lastOrderID > 0 {
req.OrderID(int64(lastOrderID))
} else {
2022-01-23 08:19:13 +00:00
req.StartTime(since.UnixNano() / int64(time.Millisecond))
2022-04-11 07:39:03 +00:00
if until.Sub(since) < 24*time.Hour {
2022-01-23 08:19:13 +00:00
req.EndTime(until.UnixNano() / int64(time.Millisecond))
}
}
2020-11-05 00:33:57 +00:00
2022-01-23 08:19:13 +00:00
// default 500, max 1000
req.Limit(1000)
binanceOrders, err := req.Do(ctx)
if err != nil {
return orders, err
}
2020-11-05 00:33:57 +00:00
return toGlobalOrders(binanceOrders, e.IsMargin)
2020-11-05 00:33:57 +00:00
}
2021-12-14 11:09:44 +00:00
func (e *Exchange) CancelOrders(ctx context.Context, orders ...types.Order) (err error) {
2023-07-25 07:09:57 +00:00
if err = orderLimiter.Wait(ctx); err != nil {
log.WithError(err).Errorf("order rate limiter wait error")
2023-07-25 07:09:57 +00:00
return err
}
2021-12-14 10:01:24 +00:00
if e.IsFutures {
return e.cancelFuturesOrders(ctx, orders...)
2021-12-14 10:01:24 +00:00
}
for _, o := range orders {
2021-12-29 09:35:27 +00:00
if e.IsMargin {
var req = e.client.NewCancelMarginOrderService()
2021-12-29 09:36:08 +00:00
req.IsIsolated(e.IsIsolatedMargin)
2021-12-29 09:35:27 +00:00
req.Symbol(o.Symbol)
2021-12-29 09:35:27 +00:00
if o.OrderID > 0 {
req.OrderID(int64(o.OrderID))
} else if len(o.ClientOrderID) > 0 {
req.OrigClientOrderID(o.ClientOrderID)
} else {
2022-01-11 10:00:07 +00:00
err = multierr.Append(err, types.NewOrderError(
fmt.Errorf("can not cancel %s order, order does not contain orderID or clientOrderID", o.Symbol),
o))
continue
2021-12-29 09:35:27 +00:00
}
_, err2 := req.Do(ctx)
if err2 != nil {
2022-01-11 10:00:07 +00:00
err = multierr.Append(err, types.NewOrderError(err2, o))
2021-12-29 09:35:27 +00:00
}
} else {
// SPOT
var req = e.client.NewCancelOrderService()
2021-12-29 09:35:27 +00:00
req.Symbol(o.Symbol)
if o.OrderID > 0 {
req.OrderID(int64(o.OrderID))
} else if len(o.ClientOrderID) > 0 {
req.OrigClientOrderID(o.ClientOrderID)
} else {
2022-01-11 10:00:07 +00:00
err = multierr.Append(err, types.NewOrderError(
fmt.Errorf("can not cancel %s order, order does not contain orderID or clientOrderID", o.Symbol),
o))
continue
2021-12-29 09:35:27 +00:00
}
_, err2 := req.Do(ctx)
if err2 != nil {
2022-01-11 10:00:07 +00:00
err = multierr.Append(err, types.NewOrderError(err2, o))
2021-12-29 09:35:27 +00:00
}
}
}
2021-12-14 11:09:44 +00:00
return err
}
2021-01-15 07:11:38 +00:00
func (e *Exchange) submitMarginOrder(ctx context.Context, order types.SubmitOrder) (*types.Order, error) {
2021-01-15 07:46:54 +00:00
orderType, err := toLocalOrderType(order.Type)
if err != nil {
return nil, err
}
req := e.client.NewCreateMarginOrderService().
2021-01-15 07:46:54 +00:00
Symbol(order.Symbol).
Type(orderType).
2021-06-06 17:06:38 +00:00
Side(binance.SideType(order.Side))
clientOrderID := newSpotClientOrderID(order.ClientOrderID)
if len(clientOrderID) > 0 {
req.NewClientOrderID(clientOrderID)
}
2021-01-15 07:46:54 +00:00
// use response result format
req.NewOrderRespType(binance.NewOrderRespTypeRESULT)
2021-01-19 18:09:12 +00:00
if e.IsIsolatedMargin {
req.IsIsolated(e.IsIsolatedMargin)
2021-01-15 07:46:54 +00:00
}
if len(order.MarginSideEffect) > 0 {
req.SideEffectType(binance.SideEffectType(order.MarginSideEffect))
}
2022-01-10 17:36:19 +00:00
if order.Market.Symbol != "" {
2021-01-15 23:37:42 +00:00
req.Quantity(order.Market.FormatQuantity(order.Quantity))
} else {
// TODO report error
req.Quantity(order.Quantity.FormatString(8))
2021-01-15 23:37:42 +00:00
}
2021-01-15 07:46:54 +00:00
2021-02-10 16:21:56 +00:00
// set price field for limit orders
switch order.Type {
case types.OrderTypeStopLimit, types.OrderTypeLimit, types.OrderTypeLimitMaker:
2022-01-10 17:36:19 +00:00
if order.Market.Symbol != "" {
2021-02-10 16:21:56 +00:00
req.Price(order.Market.FormatPrice(order.Price))
2022-01-10 17:36:19 +00:00
} else {
// TODO report error
req.Price(order.Price.FormatString(8))
2021-02-10 16:21:56 +00:00
}
2021-01-15 07:46:54 +00:00
}
2021-02-10 16:21:56 +00:00
// set stop price
2021-01-15 07:46:54 +00:00
switch order.Type {
2021-02-10 16:21:56 +00:00
2021-01-15 07:46:54 +00:00
case types.OrderTypeStopLimit, types.OrderTypeStopMarket:
2022-01-10 17:36:19 +00:00
if order.Market.Symbol != "" {
req.StopPrice(order.Market.FormatPrice(order.StopPrice))
} else {
// TODO report error
req.StopPrice(order.StopPrice.FormatString(8))
2021-01-15 07:46:54 +00:00
}
}
2021-01-15 23:37:42 +00:00
// could be IOC or FOK
2021-01-15 07:46:54 +00:00
if len(order.TimeInForce) > 0 {
// TODO: check the TimeInForce value
req.TimeInForce(binance.TimeInForceType(order.TimeInForce))
} else {
switch order.Type {
case types.OrderTypeLimit, types.OrderTypeStopLimit:
req.TimeInForce(binance.TimeInForceTypeGTC)
}
2021-01-15 07:46:54 +00:00
}
response, err := req.Do(ctx)
if err != nil {
return nil, err
}
log.Infof("margin order creation response: %+v", response)
createdOrder, err := toGlobalOrder(&binance.Order{
2021-01-15 07:46:54 +00:00
Symbol: response.Symbol,
OrderID: response.OrderID,
ClientOrderID: response.ClientOrderID,
Price: response.Price,
OrigQuantity: response.OrigQuantity,
ExecutedQuantity: response.ExecutedQuantity,
CummulativeQuoteQuantity: response.CummulativeQuoteQuantity,
Status: response.Status,
TimeInForce: response.TimeInForce,
Type: response.Type,
Side: response.Side,
UpdateTime: response.TransactTime,
Time: response.TransactTime,
2021-01-19 17:24:29 +00:00
IsIsolated: response.IsIsolated,
}, true)
2021-01-15 07:46:54 +00:00
return createdOrder, err
2021-01-15 07:11:38 +00:00
}
2020-07-11 05:02:53 +00:00
2021-04-28 11:20:55 +00:00
// BBGO is a broker on Binance
const spotBrokerID = "NSUYEBKM"
func newSpotClientOrderID(originalID string) (clientOrderID string) {
if originalID == types.NoClientOrderID {
return ""
}
2021-04-28 11:20:55 +00:00
prefix := "x-" + spotBrokerID
prefixLen := len(prefix)
if originalID != "" {
// try to keep the whole original client order ID if user specifies it.
if prefixLen+len(originalID) > 32 {
2021-04-28 11:20:55 +00:00
return originalID
}
clientOrderID = prefix + originalID
return clientOrderID
}
clientOrderID = uuid.New().String()
clientOrderID = prefix + clientOrderID
if len(clientOrderID) > 32 {
return clientOrderID[0:32]
}
return clientOrderID
}
2021-01-15 07:11:38 +00:00
func (e *Exchange) submitSpotOrder(ctx context.Context, order types.SubmitOrder) (*types.Order, error) {
orderType, err := toLocalOrderType(order.Type)
if err != nil {
return nil, err
}
req := e.client.NewCreateOrderService().
2021-01-15 07:11:38 +00:00
Symbol(order.Symbol).
Side(binance.SideType(order.Side)).
Type(orderType)
2020-07-11 05:02:53 +00:00
2021-06-06 17:06:38 +00:00
clientOrderID := newSpotClientOrderID(order.ClientOrderID)
if len(clientOrderID) > 0 {
req.NewClientOrderID(clientOrderID)
}
2022-01-10 17:36:19 +00:00
if order.Market.Symbol != "" {
req.Quantity(order.Market.FormatQuantity(order.Quantity))
} else {
// TODO: report error
req.Quantity(order.Quantity.FormatString(8))
}
2020-07-11 05:02:53 +00:00
// set price field for limit orders
switch order.Type {
case types.OrderTypeStopLimit, types.OrderTypeLimit, types.OrderTypeLimitMaker:
2022-01-10 17:36:19 +00:00
if order.Market.Symbol != "" {
req.Price(order.Market.FormatPrice(order.Price))
2022-01-10 17:36:19 +00:00
} else {
// TODO: report error
req.Price(order.Price.FormatString(8))
}
2021-01-15 07:11:38 +00:00
}
2020-12-03 01:25:47 +00:00
2021-01-15 07:11:38 +00:00
switch order.Type {
case types.OrderTypeStopLimit, types.OrderTypeStopMarket:
2022-01-10 17:36:19 +00:00
if order.Market.Symbol != "" {
req.StopPrice(order.Market.FormatPrice(order.StopPrice))
} else {
// TODO: report error
req.StopPrice(order.StopPrice.FormatString(8))
2020-12-03 01:25:47 +00:00
}
2021-01-15 07:11:38 +00:00
}
2020-07-11 05:02:53 +00:00
2021-01-15 07:11:38 +00:00
if len(order.TimeInForce) > 0 {
// TODO: check the TimeInForce value
req.TimeInForce(binance.TimeInForceType(order.TimeInForce))
} else {
switch order.Type {
case types.OrderTypeLimit, types.OrderTypeStopLimit:
req.TimeInForce(binance.TimeInForceTypeGTC)
}
2021-01-15 07:11:38 +00:00
}
2020-07-11 07:27:26 +00:00
req.NewOrderRespType(binance.NewOrderRespTypeRESULT)
2021-01-15 07:11:38 +00:00
response, err := req.Do(ctx)
if err != nil {
return nil, err
}
2020-12-29 09:26:22 +00:00
log.Infof("spot order creation response: %+v", response)
2021-01-15 07:11:38 +00:00
createdOrder, err := toGlobalOrder(&binance.Order{
2021-01-15 07:11:38 +00:00
Symbol: response.Symbol,
OrderID: response.OrderID,
ClientOrderID: response.ClientOrderID,
Price: response.Price,
OrigQuantity: response.OrigQuantity,
ExecutedQuantity: response.ExecutedQuantity,
CummulativeQuoteQuantity: response.CummulativeQuoteQuantity,
Status: response.Status,
TimeInForce: response.TimeInForce,
Type: response.Type,
Side: response.Side,
UpdateTime: response.TransactTime,
Time: response.TransactTime,
2021-01-19 17:24:29 +00:00
IsIsolated: response.IsIsolated,
}, false)
2021-01-15 07:11:38 +00:00
return createdOrder, err
}
func (e *Exchange) SubmitOrder(ctx context.Context, order types.SubmitOrder) (createdOrder *types.Order, err error) {
2023-07-25 07:09:57 +00:00
if err = orderLimiter.Wait(ctx); err != nil {
log.WithError(err).Errorf("order rate limiter wait error")
2023-07-25 07:09:57 +00:00
return nil, err
}
if e.IsMargin {
createdOrder, err = e.submitMarginOrder(ctx, order)
} else if e.IsFutures {
createdOrder, err = e.submitFuturesOrder(ctx, order)
} else {
createdOrder, err = e.submitSpotOrder(ctx, order)
2020-07-11 07:27:26 +00:00
}
return createdOrder, err
2020-07-11 07:27:26 +00:00
}
2020-11-06 16:49:17 +00:00
// QueryKLines queries the Kline/candlestick bars for a symbol. Klines are uniquely identified by their open time.
2021-05-05 08:23:46 +00:00
// Binance uses inclusive start time query range, eg:
// https://api.binance.com/api/v3/klines?symbol=BTCUSDT&interval=1m&startTime=1620172860000
// the above query will return a kline with startTime = 1620172860000
// and,
// https://api.binance.com/api/v3/klines?symbol=BTCUSDT&interval=1m&startTime=1620172860000&endTime=1620172920000
// the above query will return a kline with startTime = 1620172860000, and a kline with endTime = 1620172860000
//
// the endTime of a binance kline, is the (startTime + interval time - 1 millisecond), e.g.,
// millisecond unix timestamp: 1620172860000 and 1620172919999
func (e *Exchange) QueryKLines(ctx context.Context, symbol string, interval types.Interval, options types.KLineQueryOptions) ([]types.KLine, error) {
if e.IsFutures {
return e.QueryFuturesKLines(ctx, symbol, interval, options)
}
2020-10-10 04:02:06 +00:00
var limit = 1000
2020-07-15 04:20:44 +00:00
if options.Limit > 0 {
// default limit == 1000
2020-07-15 04:20:44 +00:00
limit = options.Limit
2020-07-11 12:40:19 +00:00
}
2020-08-11 00:36:36 +00:00
log.Infof("querying kline %s %s %v", symbol, interval, options)
2020-07-11 05:02:53 +00:00
req := e.client.NewKlinesService().
2020-10-06 09:32:41 +00:00
Symbol(symbol).
Interval(string(interval)).
2020-07-15 04:20:44 +00:00
Limit(limit)
if options.StartTime != nil {
req.StartTime(options.StartTime.UnixMilli())
2020-07-15 04:20:44 +00:00
}
if options.EndTime != nil {
req.EndTime(options.EndTime.UnixMilli())
2020-07-15 04:20:44 +00:00
}
resp, err := req.Do(ctx)
2020-07-11 05:02:53 +00:00
if err != nil {
return nil, err
}
var kLines []types.KLine
for _, k := range resp {
kLines = append(kLines, types.KLine{
Exchange: types.ExchangeBinance,
Symbol: symbol,
Interval: interval,
StartTime: types.NewTimeFromUnix(0, k.OpenTime*int64(time.Millisecond)),
EndTime: types.NewTimeFromUnix(0, k.CloseTime*int64(time.Millisecond)),
Open: fixedpoint.MustNewFromString(k.Open),
Close: fixedpoint.MustNewFromString(k.Close),
High: fixedpoint.MustNewFromString(k.High),
Low: fixedpoint.MustNewFromString(k.Low),
Volume: fixedpoint.MustNewFromString(k.Volume),
QuoteVolume: fixedpoint.MustNewFromString(k.QuoteAssetVolume),
TakerBuyBaseAssetVolume: fixedpoint.MustNewFromString(k.TakerBuyBaseAssetVolume),
TakerBuyQuoteAssetVolume: fixedpoint.MustNewFromString(k.TakerBuyQuoteAssetVolume),
LastTradeID: 0,
NumberOfTrades: uint64(k.TradeNum),
Closed: true,
})
}
kLines = types.SortKLinesAscending(kLines)
return kLines, nil
}
2022-05-28 17:21:43 +00:00
func (e *Exchange) queryMarginTrades(ctx context.Context, symbol string, options *types.TradeQueryOptions) (trades []types.Trade, err error) {
var remoteTrades []*binance.TradeV3
req := e.client.NewListMarginTradesService().
IsIsolated(e.IsIsolatedMargin).
Symbol(symbol)
if options.Limit > 0 {
req.Limit(int(options.Limit))
} else {
req.Limit(1000)
}
// BINANCE seems to have an API bug, we can't use both fromId and the start time/end time
2022-05-28 17:21:43 +00:00
// BINANCE uses inclusive last trade ID
if options.LastTradeID > 0 {
req.FromID(int64(options.LastTradeID))
} else {
if options.StartTime != nil && options.EndTime != nil {
if options.EndTime.Sub(*options.StartTime) < 24*time.Hour {
req.StartTime(options.StartTime.UnixMilli())
req.EndTime(options.EndTime.UnixMilli())
} else {
req.StartTime(options.StartTime.UnixMilli())
}
} else if options.StartTime != nil {
2022-05-28 17:21:43 +00:00
req.StartTime(options.StartTime.UnixMilli())
} else if options.EndTime != nil {
2022-05-28 17:21:43 +00:00
req.EndTime(options.EndTime.UnixMilli())
2021-01-19 17:27:27 +00:00
}
2022-05-28 17:21:43 +00:00
}
2022-05-28 17:21:43 +00:00
remoteTrades, err = req.Do(ctx)
if err != nil {
return nil, err
}
for _, t := range remoteTrades {
localTrade, err := toGlobalTrade(*t, e.IsMargin)
if err != nil {
log.WithError(err).Errorf("can not convert binance trade: %+v", t)
continue
2021-01-19 17:27:27 +00:00
}
2022-05-28 17:21:43 +00:00
trades = append(trades, *localTrade)
}
trades = types.SortTradesAscending(trades)
return trades, nil
}
func (e *Exchange) querySpotTrades(ctx context.Context, symbol string, options *types.TradeQueryOptions) (trades []types.Trade, err error) {
2022-12-09 08:44:27 +00:00
req := e.client2.NewGetMyTradesRequest()
req.Symbol(symbol)
2021-01-19 17:27:27 +00:00
2022-05-28 17:21:43 +00:00
// BINANCE uses inclusive last trade ID
if options.LastTradeID > 0 {
2022-12-09 08:44:27 +00:00
req.FromID(options.LastTradeID)
} else {
if options.StartTime != nil && options.EndTime != nil {
if options.EndTime.Sub(*options.StartTime) < 24*time.Hour {
req.StartTime(*options.StartTime)
req.EndTime(*options.EndTime)
} else {
req.StartTime(*options.StartTime)
}
} else if options.StartTime != nil {
2022-12-09 08:44:27 +00:00
req.StartTime(*options.StartTime)
} else if options.EndTime != nil {
2022-12-09 08:44:27 +00:00
req.EndTime(*options.EndTime)
}
2022-05-28 17:21:43 +00:00
}
2022-12-09 08:44:27 +00:00
if options.Limit > 0 {
req.Limit(uint64(options.Limit))
} else {
req.Limit(1000)
}
remoteTrades, err := req.Do(ctx)
2022-05-28 17:21:43 +00:00
if err != nil {
return nil, err
}
2022-12-09 08:44:27 +00:00
2022-05-28 17:21:43 +00:00
for _, t := range remoteTrades {
2022-12-09 08:44:27 +00:00
localTrade, err := toGlobalTrade(t, e.IsMargin)
2021-01-19 17:27:27 +00:00
if err != nil {
2022-05-28 17:21:43 +00:00
log.WithError(err).Errorf("can not convert binance trade: %+v", t)
continue
2021-01-19 17:27:27 +00:00
}
2022-05-28 17:21:43 +00:00
trades = append(trades, *localTrade)
}
trades = types.SortTradesAscending(trades)
return trades, nil
}
2022-09-12 07:03:01 +00:00
func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *types.TradeQueryOptions) ([]types.Trade, error) {
if err := queryTradeLimiter.Wait(ctx); err != nil {
return nil, err
}
2022-05-28 17:21:43 +00:00
if e.IsMargin {
return e.queryMarginTrades(ctx, symbol, options)
} else if e.IsFutures {
return e.queryFuturesTrades(ctx, symbol, options)
}
2022-09-12 07:03:01 +00:00
return e.querySpotTrades(ctx, symbol, options)
}
// DefaultFeeRates returns the Binance VIP 0 fee schedule
// See also https://www.binance.com/en/fee/schedule
2023-03-24 10:14:52 +00:00
// See futures fee at: https://www.binance.com/en/fee/futureFee
func (e *Exchange) DefaultFeeRates() types.ExchangeFee {
2023-03-24 10:14:24 +00:00
if e.IsFutures {
return types.ExchangeFee{
MakerFeeRate: fixedpoint.NewFromFloat(0.01 * 0.0180), // 0.0180% -USDT with BNB
TakerFeeRate: fixedpoint.NewFromFloat(0.01 * 0.0360), // 0.0360% -USDT with BNB
}
}
return types.ExchangeFee{
2023-03-24 10:14:24 +00:00
MakerFeeRate: fixedpoint.NewFromFloat(0.01 * 0.075), // 0.075% with BNB
TakerFeeRate: fixedpoint.NewFromFloat(0.01 * 0.075), // 0.075% with BNB
}
}
2022-04-21 07:29:59 +00:00
// QueryDepth query the order book depth of a symbol
func (e *Exchange) QueryDepth(ctx context.Context, symbol string) (snapshot types.SliceOrderBook, finalUpdateID int64, err error) {
if e.IsFutures {
return e.queryFuturesDepth(ctx, symbol)
}
response, err := e.client.NewDepthService().Symbol(symbol).Do(ctx)
if err != nil {
return snapshot, finalUpdateID, err
}
return convertDepth(snapshot, symbol, finalUpdateID, response)
}
func convertDepth(snapshot types.SliceOrderBook, symbol string, finalUpdateID int64, response *binance.DepthResponse) (types.SliceOrderBook, int64, error) {
snapshot.Symbol = symbol
// empty time since the API does not provide time information.
snapshot.Time = time.Time{}
finalUpdateID = response.LastUpdateID
for _, entry := range response.Bids {
// entry.Price, Quantity: entry.Quantity
price, err := fixedpoint.NewFromString(entry.Price)
if err != nil {
return snapshot, finalUpdateID, err
}
quantity, err := fixedpoint.NewFromString(entry.Quantity)
if err != nil {
return snapshot, finalUpdateID, err
}
snapshot.Bids = append(snapshot.Bids, types.PriceVolume{Price: price, Volume: quantity})
}
for _, entry := range response.Asks {
price, err := fixedpoint.NewFromString(entry.Price)
if err != nil {
return snapshot, finalUpdateID, err
}
quantity, err := fixedpoint.NewFromString(entry.Quantity)
if err != nil {
return snapshot, finalUpdateID, err
}
snapshot.Asks = append(snapshot.Asks, types.PriceVolume{Price: price, Volume: quantity})
}
return snapshot, finalUpdateID, nil
}
2022-05-28 17:21:43 +00:00
// QueryPremiumIndex is only for futures
func (e *Exchange) QueryPremiumIndex(ctx context.Context, symbol string) (*types.PremiumIndex, error) {
2021-10-20 06:01:19 +00:00
// when symbol is set, only one index will be returned.
2022-05-28 17:21:43 +00:00
indexes, err := e.futuresClient.NewPremiumIndexService().Symbol(symbol).Do(ctx)
2021-10-19 07:54:16 +00:00
if err != nil {
return nil, err
}
2021-10-20 06:01:19 +00:00
return convertPremiumIndex(indexes[0])
2021-10-19 07:54:16 +00:00
}
func (e *Exchange) QueryFundingRateHistory(ctx context.Context, symbol string) (*types.FundingRate, error) {
2022-05-28 17:21:43 +00:00
rates, err := e.futuresClient.NewFundingRateService().
Symbol(symbol).
Limit(1).
Do(ctx)
if err != nil {
2021-10-14 15:01:10 +00:00
return nil, err
}
if len(rates) == 0 {
2021-10-14 15:01:10 +00:00
return nil, errors.New("empty funding rate data")
}
rate := rates[0]
fundingRate, err := fixedpoint.NewFromString(rate.FundingRate)
if err != nil {
return nil, err
}
return &types.FundingRate{
2021-10-14 15:01:10 +00:00
FundingRate: fundingRate,
FundingTime: time.Unix(0, rate.FundingTime*int64(time.Millisecond)),
Time: time.Unix(0, rate.Time*int64(time.Millisecond)),
}, nil
}
2022-01-03 19:30:36 +00:00
func (e *Exchange) QueryPositionRisk(ctx context.Context, symbol string) (*types.PositionRisk, error) {
// when symbol is set, only one position risk will be returned.
2022-05-28 17:21:43 +00:00
risks, err := e.futuresClient.NewGetPositionRiskService().Symbol(symbol).Do(ctx)
2022-01-03 19:30:36 +00:00
if err != nil {
return nil, err
}
return convertPositionRisk(risks[0])
}
// in seconds
var SupportedIntervals = map[types.Interval]int{
types.Interval1s: 1,
types.Interval1m: 1 * 60,
types.Interval5m: 5 * 60,
types.Interval15m: 15 * 60,
types.Interval30m: 30 * 60,
types.Interval1h: 60 * 60,
types.Interval2h: 60 * 60 * 2,
types.Interval4h: 60 * 60 * 4,
types.Interval6h: 60 * 60 * 6,
types.Interval12h: 60 * 60 * 12,
types.Interval1d: 60 * 60 * 24,
types.Interval3d: 60 * 60 * 24 * 3,
types.Interval1w: 60 * 60 * 24 * 7,
}
func (e *Exchange) SupportedInterval() map[types.Interval]int {
return SupportedIntervals
}
func (e *Exchange) IsSupportedInterval(interval types.Interval) bool {
_, ok := SupportedIntervals[interval]
return ok
}
func getLaunchDate() (time.Time, error) {
// binance launch date 12:00 July 14th, 2017
loc, err := time.LoadLocation("Asia/Shanghai")
if err != nil {
return time.Time{}, err
}
return time.Date(2017, time.July, 14, 0, 0, 0, 0, loc), nil
}
2022-04-22 06:45:03 +00:00
// Margin tolerance ranges from 0.0 (liquidation) to 1.0 (safest level of margin).
func calculateMarginTolerance(marginLevel fixedpoint.Value) fixedpoint.Value {
if marginLevel.IsZero() {
// Although margin level shouldn't be zero, that would indicate a significant problem.
// In that case, margin tolerance should return 0.0 to also reflect that problem.
return fixedpoint.Zero
}
// Formula created by operations team for our binance code. Liquidation occurs at 1.1,
// so when marginLevel equals 1.1, the formula becomes 1.0 - 1.0, or zero.
// = 1.0 - (1.1 / marginLevel)
return fixedpoint.One.Sub(fixedpoint.NewFromFloat(1.1).Div(marginLevel))
}
func logResponse(resp interface{}, err error, req interface{}) error {
if err != nil {
log.WithError(err).Errorf("%T: error %+v", req, resp)
return err
}
log.Infof("%T: response: %+v", req, resp)
return nil
}