2024-08-30 14:55:17 +00:00
package binance
import (
"context"
"fmt"
2024-08-30 15:10:58 +00:00
"net/http"
"net/url"
2024-08-30 14:55:17 +00:00
"strconv"
"strings"
"sync"
"time"
"github.com/adshao/go-binance/v2"
"github.com/adshao/go-binance/v2/futures"
"github.com/spf13/viper"
"go.uber.org/multierr"
"golang.org/x/time/rate"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"git.qtrade.icu/lychiyu/bbgo/pkg/exchange/binance/binanceapi"
"git.qtrade.icu/lychiyu/bbgo/pkg/fixedpoint"
"git.qtrade.icu/lychiyu/bbgo/pkg/types"
"git.qtrade.icu/lychiyu/bbgo/pkg/util"
)
const BNB = "BNB"
const DefaultDepthLimit = 5000
const BinanceUSBaseURL = "https://api.binance.us"
const BinanceTestBaseURL = "https://testnet.binance.vision"
const BinanceUSWebSocketURL = "wss://stream.binance.us:9443"
const WebSocketURL = "wss://stream.binance.com:9443"
const WebSocketTestURL = "wss://testnet.binance.vision"
const FutureTestBaseURL = "https://testnet.binancefuture.com"
const FuturesWebSocketURL = "wss://fstream.binance.com"
const FuturesWebSocketTestURL = "wss://stream.binancefuture.com"
// 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 )
var queryTradeLimiter = rate . NewLimiter ( 1 , 2 )
var log = logrus . WithFields ( logrus . Fields {
"exchange" : "binance" ,
} )
func init ( ) {
_ = types . Exchange ( & Exchange { } )
_ = types . MarginExchange ( & Exchange { } )
_ = types . FuturesExchange ( & Exchange { } )
if n , ok := util . GetEnvVarInt ( "BINANCE_ORDER_RATE_LIMITER" ) ; ok {
orderLimiter = rate . NewLimiter ( rate . Every ( time . Duration ( n ) * time . Minute ) , 2 )
}
if n , ok := util . GetEnvVarInt ( "BINANCE_QUERY_TRADES_RATE_LIMITER" ) ; ok {
queryTradeLimiter = rate . NewLimiter ( rate . Every ( time . Duration ( n ) * time . Minute ) , 2 )
}
}
func isBinanceUs ( ) bool {
v , ok := util . GetEnvVarBool ( "BINANCE_US" )
return ok && v
}
type Exchange struct {
types . MarginSettings
types . FuturesSettings
key , secret string
// client is used for spot & margin
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
}
var timeSetterOnce sync . Once
2024-08-30 15:10:58 +00:00
func New ( key , secret , proxy string ) * Exchange {
2024-08-30 14:55:17 +00:00
if util . IsPaperTrade ( ) {
binance . UseTestnet = true
}
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" )
if isBinanceUs ( ) {
client . BaseURL = BinanceUSBaseURL
}
client2 := binanceapi . NewClient ( client . BaseURL )
futuresClient2 := binanceapi . NewFuturesRestClient ( futuresClient . BaseURL )
2024-08-30 15:10:58 +00:00
if proxy != "" {
proxyURL , err := url . Parse ( proxy )
if err == nil {
proxyTransport := & http . Transport {
Proxy : http . ProxyURL ( proxyURL ) ,
}
client . HTTPClient . Transport = proxyTransport
futuresClient . HTTPClient . Transport = proxyTransport
client2 . HttpClient . Transport = proxyTransport
futuresClient2 . HttpClient . Transport = proxyTransport
}
}
2024-08-30 14:55:17 +00:00
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 )
futuresClient2 . Auth ( key , secret )
}
ctx := context . Background ( )
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" )
}
}
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
}
return toGlobalTicker ( stats [ 0 ] )
}
func ( e * Exchange ) QueryTickers ( ctx context . Context , symbol ... string ) ( map [ string ] types . Ticker , error ) {
var tickers = make ( map [ string ] types . Ticker )
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
}
m := make ( map [ string ] struct { } )
exists := struct { } { }
for _ , s := range symbol {
m [ s ] = exists
}
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
}
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 . BidPrice ) ,
Sell : fixedpoint . MustNewFromString ( stats . AskPrice ) ,
Time : time . Unix ( 0 , stats . CloseTime * int64 ( time . Millisecond ) ) ,
}
tickers [ stats . Symbol ] = tick
}
return tickers , nil
}
func ( e * Exchange ) QueryMarkets ( ctx context . Context ) ( types . MarketMap , error ) {
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 )
if err != nil {
return nil , err
}
markets := types . MarketMap { }
for _ , symbol := range exchangeInfo . Symbols {
markets [ symbol . Symbol ] = toGlobalMarket ( symbol )
}
return markets , nil
}
func ( e * Exchange ) QueryAveragePrice ( ctx context . Context , symbol string ) ( fixedpoint . Value , error ) {
resp , err := e . client . NewAveragePriceService ( ) . Symbol ( symbol ) . Do ( ctx )
if err != nil {
return fixedpoint . Zero , err
}
return fixedpoint . MustNewFromString ( resp . Price ) , nil
}
func ( e * Exchange ) NewStream ( ) types . Stream {
stream := NewStream ( e , e . client , e . futuresClient )
stream . MarginSettings = e . MarginSettings
stream . FuturesSettings = e . FuturesSettings
return stream
}
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 {
return fixedpoint . Zero , err
}
return resp . Amount , nil
}
func ( e * Exchange ) borrowRepayAsset (
ctx context . Context , asset string , amount fixedpoint . Value , marginType binanceapi . BorrowRepayType ,
) error {
req := e . client2 . NewPlaceMarginOrderRequest ( )
req . Asset ( asset )
req . Amount ( amount )
req . SetBorrowRepayType ( marginType )
if e . IsIsolatedMargin {
req . IsIsolated ( e . IsIsolatedMargin )
req . Symbol ( e . IsolatedMarginSymbol )
}
log . Infof ( "%s margin asset %s amount %f" , marginType , asset , amount . Float64 ( ) )
resp , err := req . Do ( ctx )
if err != nil {
return err
}
log . Debugf ( "margin %s %f %s, transaction id = %d" , marginType , amount . Float64 ( ) , asset , resp . TranId )
return err
}
func ( e * Exchange ) RepayMarginAsset ( ctx context . Context , asset string , amount fixedpoint . Value ) error {
return e . borrowRepayAsset ( ctx , asset , amount , binanceapi . BorrowRepayTypeRepay )
}
func ( e * Exchange ) BorrowMarginAsset ( ctx context . Context , asset string , amount fixedpoint . Value ) error {
return e . borrowRepayAsset ( ctx , asset , amount , binanceapi . BorrowRepayTypeBorrow )
}
func ( e * Exchange ) QueryMarginBorrowHistory ( ctx context . Context , asset string ) error {
req := e . client2 . NewGetMarginBorrowRepayHistoryRequest ( )
req . SetBorrowRepayType ( binanceapi . BorrowRepayTypeBorrow )
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 )
}
return e . transferCrossMarginAccountAsset ( ctx , asset , amount , io )
}
func ( e * Exchange ) transferIsolatedMarginAccountAsset (
ctx context . Context , asset string , amount fixedpoint . Value , io types . TransferDirection ,
) error {
req := e . client2 . NewTransferAssetRequest ( )
req . Asset ( asset )
req . FromSymbol ( e . IsolatedMarginSymbol )
req . ToSymbol ( e . IsolatedMarginSymbol )
switch io {
case types . TransferIn :
req . TransferType ( binanceapi . TransferAssetTypeMainToIsolatedMargin )
case types . TransferOut :
req . TransferType ( binanceapi . TransferAssetTypeIsolatedMarginToMain )
}
req . Asset ( asset )
req . Amount ( amount . String ( ) )
resp , err := req . Do ( ctx )
return logResponse ( resp , err , req )
}
// 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 . NewTransferAssetRequest ( )
req . Asset ( asset )
req . Amount ( amount . String ( ) )
if io == types . TransferIn {
req . TransferType ( binanceapi . TransferAssetTypeMainToMargin )
} else if io == types . TransferOut {
req . TransferType ( binanceapi . TransferAssetTypeMarginToMain )
} 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
}
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.
MarginLevel : marginLevel ,
MarginTolerance : calculateMarginTolerance ( marginLevel ) ,
BorrowEnabled : marginAccount . BorrowEnabled ,
TransferEnabled : marginAccount . TransferEnabled ,
}
// convert cross margin user assets into balances
balances := types . BalanceMap { }
for _ , userAsset := range marginAccount . UserAssets {
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 ) ,
}
}
a . UpdateBalances ( balances )
return a , nil
}
func ( e * Exchange ) QueryIsolatedMarginAccount ( ctx context . Context ) ( * types . Account , error ) {
req := e . client . NewGetIsolatedMarginAccountService ( )
req . Symbols ( e . IsolatedMarginSymbol )
marginAccount , err := req . Do ( ctx )
if err != nil {
return nil , err
}
a := & types . Account {
AccountType : types . AccountTypeIsolatedMargin ,
IsolatedMarginInfo : toGlobalIsolatedMarginAccountInfo ( marginAccount ) , // In binance GO api, Account define marginAccount info which mantain []*AccountAsset and []*AccountPosition.
}
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 ) )
}
userAsset := marginAccount . Assets [ 0 ]
marginLevel := fixedpoint . MustNewFromString ( userAsset . MarginLevel )
a . MarginLevel = marginLevel
a . MarginTolerance = calculateMarginTolerance ( marginLevel )
a . MarginRatio = fixedpoint . MustNewFromString ( userAsset . MarginRatio )
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 ) ,
}
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 ) ,
}
a . UpdateBalances ( balances )
return a , nil
}
func ( e * Exchange ) Withdraw (
ctx context . Context , asset string , amount fixedpoint . Value , address string , options * types . WithdrawalOptions ,
) error {
req := e . client2 . NewWithdrawRequest ( )
req . Coin ( asset )
req . Address ( address )
req . Amount ( fmt . Sprintf ( "%f" , amount . Float64 ( ) ) )
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 )
}
req := e . client2 . NewGetWithdrawHistoryRequest ( )
if len ( asset ) > 0 {
req . Coin ( asset )
}
records , err := req .
StartTime ( since ) .
EndTime ( until ) .
Limit ( 1000 ) .
Do ( ctx )
if err != nil {
return withdraws , err
}
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
}
status , err := toGlobalWithdrawStatus ( d . Status )
if err != nil {
return nil , err
}
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 : status ,
OriginalStatus : fmt . Sprintf ( "%s (%d)" , d . Status . String ( ) , int ( d . Status ) ) ,
} )
}
return withdraws , nil
}
func ( e * Exchange ) QueryDepositHistory ( ctx context . Context , asset string , since , until time . Time ) ( allDeposits [ ] types . Deposit , err error ) {
if since . IsZero ( ) {
since , err = getLaunchDate ( )
if err != nil {
return nil , err
}
}
// startTime ~ endTime must be in 90 days
historyDayRangeLimit := time . Hour * 24 * 89
if until . Sub ( since ) >= historyDayRangeLimit {
until = since . Add ( historyDayRangeLimit )
}
req := e . client2 . NewGetDepositHistoryRequest ( )
if len ( asset ) > 0 {
req . Coin ( asset )
}
req . StartTime ( since ) .
EndTime ( until )
records , err := req . Do ( ctx )
if err != nil {
return nil , err
}
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
}
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 ,
} )
}
return allDeposits , nil
}
func ( e * Exchange ) QueryAccountBalances ( ctx context . Context ) ( types . BalanceMap , error ) {
account , err := e . QueryAccount ( ctx )
if err != nil {
return nil , err
}
return account . Balances ( ) , nil
}
func ( e * Exchange ) PlatformFeeCurrency ( ) string {
return BNB
}
func ( e * Exchange ) QuerySpotAccount ( ctx context . Context ) ( * types . Account , error ) {
account , err := e . client . NewGetAccountService ( ) . Do ( ctx )
if err != nil {
return nil , err
}
var balances = map [ string ] types . Balance { }
for _ , b := range account . Balances {
balances [ b . Asset ] = types . Balance {
Currency : b . Asset ,
Available : fixedpoint . MustNewFromString ( b . Free ) ,
Locked : fixedpoint . MustNewFromString ( b . Locked ) ,
}
}
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
}
a . UpdateBalances ( balances )
return a , nil
}
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 )
}
return account , err
}
func ( e * Exchange ) QueryOpenOrders ( ctx context . Context , symbol string ) ( orders [ ] types . Order , err error ) {
if e . IsMargin {
req := e . client . NewListMarginOpenOrdersService ( ) . Symbol ( symbol )
req . IsIsolated ( e . IsIsolatedMargin )
binanceOrders , err := req . Do ( ctx )
if err != nil {
return orders , err
}
return toGlobalOrders ( binanceOrders , false )
}
if e . IsFutures {
req := e . futuresClient . NewListOpenOrdersService ( ) . Symbol ( symbol )
binanceOrders , err := req . Do ( ctx )
if err != nil {
return orders , err
}
return toGlobalFuturesOrders ( binanceOrders , false )
}
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
}
if len ( q . Symbol ) == 0 {
return nil , errors . New ( "binance: symbol parameter is a mandatory parameter for querying order trades" )
}
var remoteTrades [ ] binance . TradeV3
var trades [ ] types . Trade
if e . IsMargin {
req := e . client2 . NewGetMarginTradesRequest ( )
req . Symbol ( q . Symbol ) . OrderID ( uint64 ( orderID ) )
if e . IsIsolatedMargin {
req . IsIsolated ( true )
}
remoteTrades , err = req . Do ( ctx )
if err != nil {
return nil , err
}
} else {
req := e . client2 . NewGetMyTradesRequest ( )
req . Symbol ( q . Symbol ) .
OrderID ( uint64 ( orderID ) )
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 ( "binance: unable to convert margin trade: %+v" , t )
continue
}
trades = append ( trades , * localTrade )
}
return types . SortTradesAscending ( 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
}
if e . IsMargin {
order , err := e . client . NewGetMarginOrderService ( ) . Symbol ( q . Symbol ) . OrderID ( orderID ) . Do ( ctx )
if err != nil {
return nil , err
}
return toGlobalOrder ( order , e . IsMargin )
}
if e . IsFutures {
order , err := e . futuresClient . NewGetOrderService ( ) . Symbol ( q . Symbol ) . OrderID ( orderID ) . Do ( ctx )
if err != nil {
return nil , err
}
return toGlobalFuturesOrder ( order , false )
}
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 ) {
// 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 )
}
* /
if err = orderLimiter . Wait ( ctx ) ; err != nil {
log . WithError ( err ) . Errorf ( "order rate limiter wait error" )
return nil , err
}
log . Infof ( "querying closed orders %s from %s <=> %s ..." , symbol , since , until )
if e . IsMargin {
req := e . client . NewListMarginOrdersService ( ) . Symbol ( symbol )
req . IsIsolated ( e . IsIsolatedMargin )
if lastOrderID > 0 {
req . OrderID ( int64 ( lastOrderID ) )
} else {
req . StartTime ( since . UnixNano ( ) / int64 ( time . Millisecond ) )
if until . Sub ( since ) < 24 * time . Hour {
req . EndTime ( until . UnixNano ( ) / int64 ( time . Millisecond ) )
}
}
binanceOrders , err := req . Do ( ctx )
if err != nil {
return orders , err
}
return toGlobalOrders ( binanceOrders , e . IsMargin )
}
if e . IsFutures {
return e . queryFuturesClosedOrders ( ctx , symbol , since , until , lastOrderID )
}
// 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 )
if lastOrderID > 0 {
req . OrderID ( int64 ( lastOrderID ) )
} else {
req . StartTime ( since . UnixNano ( ) / int64 ( time . Millisecond ) )
if until . Sub ( since ) < 24 * time . Hour {
req . EndTime ( until . UnixNano ( ) / int64 ( time . Millisecond ) )
}
}
// default 500, max 1000
req . Limit ( 1000 )
binanceOrders , err := req . Do ( ctx )
if err != nil {
return orders , err
}
return toGlobalOrders ( binanceOrders , e . IsMargin )
}
func ( e * Exchange ) CancelOrders ( ctx context . Context , orders ... types . Order ) ( err error ) {
if err = orderLimiter . Wait ( ctx ) ; err != nil {
log . WithError ( err ) . Errorf ( "order rate limiter wait error" )
return err
}
if e . IsFutures {
return e . cancelFuturesOrders ( ctx , orders ... )
}
for _ , o := range orders {
if e . IsMargin {
var req = e . client . NewCancelMarginOrderService ( )
req . IsIsolated ( e . IsIsolatedMargin )
req . Symbol ( o . Symbol )
if o . OrderID > 0 {
req . OrderID ( int64 ( o . OrderID ) )
} else if len ( o . ClientOrderID ) > 0 {
req . OrigClientOrderID ( o . ClientOrderID )
} else {
err = multierr . Append ( err , types . NewOrderError (
fmt . Errorf ( "can not cancel %s order, order does not contain orderID or clientOrderID" , o . Symbol ) ,
o ) )
continue
}
_ , err2 := req . Do ( ctx )
if err2 != nil {
err = multierr . Append ( err , types . NewOrderError ( err2 , o ) )
}
} else {
// SPOT
var req = e . client . NewCancelOrderService ( )
req . Symbol ( o . Symbol )
if o . OrderID > 0 {
req . OrderID ( int64 ( o . OrderID ) )
} else if len ( o . ClientOrderID ) > 0 {
req . OrigClientOrderID ( o . ClientOrderID )
} else {
err = multierr . Append ( err , types . NewOrderError (
fmt . Errorf ( "can not cancel %s order, order does not contain orderID or clientOrderID" , o . Symbol ) ,
o ) )
continue
}
_ , err2 := req . Do ( ctx )
if err2 != nil {
err = multierr . Append ( err , types . NewOrderError ( err2 , o ) )
}
}
}
return err
}
func ( e * Exchange ) submitMarginOrder ( ctx context . Context , order types . SubmitOrder ) ( * types . Order , error ) {
orderType , err := toLocalOrderType ( order . Type )
if err != nil {
return nil , err
}
req := e . client . NewCreateMarginOrderService ( ) .
Symbol ( order . Symbol ) .
Type ( orderType ) .
Side ( binance . SideType ( order . Side ) )
clientOrderID := newSpotClientOrderID ( order . ClientOrderID )
if len ( clientOrderID ) > 0 {
req . NewClientOrderID ( clientOrderID )
}
// use response result format
req . NewOrderRespType ( binance . NewOrderRespTypeRESULT )
if e . IsIsolatedMargin {
req . IsIsolated ( e . IsIsolatedMargin )
}
if len ( order . MarginSideEffect ) > 0 {
req . SideEffectType ( binance . SideEffectType ( order . MarginSideEffect ) )
}
if order . Market . Symbol != "" {
req . Quantity ( order . Market . FormatQuantity ( order . Quantity ) )
} else {
// TODO report error
req . Quantity ( order . Quantity . FormatString ( 8 ) )
}
// set price field for limit orders
switch order . Type {
case types . OrderTypeStopLimit , types . OrderTypeLimit , types . OrderTypeLimitMaker :
if order . Market . Symbol != "" {
req . Price ( order . Market . FormatPrice ( order . Price ) )
} else {
// TODO report error
req . Price ( order . Price . FormatString ( 8 ) )
}
}
// set stop price
switch order . Type {
case types . OrderTypeStopLimit , types . OrderTypeStopMarket :
if order . Market . Symbol != "" {
req . StopPrice ( order . Market . FormatPrice ( order . StopPrice ) )
} else {
// TODO report error
req . StopPrice ( order . StopPrice . FormatString ( 8 ) )
}
}
// could be IOC or FOK
switch order . Type {
case types . OrderTypeLimit , types . OrderTypeStopLimit :
req . TimeInForce ( binance . TimeInForceTypeGTC )
case types . OrderTypeLimitMaker :
// do not set TimeInForce for LimitMaker
default :
if len ( order . TimeInForce ) > 0 {
// TODO: check the TimeInForce value
req . TimeInForce ( binance . TimeInForceType ( order . TimeInForce ) )
}
}
response , err := req . Do ( ctx )
if err != nil {
return nil , err
}
log . Infof ( "margin order creation response: %+v" , response )
createdOrder , err := toGlobalOrder ( & binance . Order {
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 ,
IsIsolated : response . IsIsolated ,
} , true )
return createdOrder , err
}
// BBGO is a broker on Binance
const spotBrokerID = "NSUYEBKM"
func newSpotClientOrderID ( originalID string ) ( clientOrderID string ) {
if originalID == types . NoClientOrderID {
return ""
}
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 {
return originalID
}
clientOrderID = prefix + originalID
return clientOrderID
}
clientOrderID = uuid . New ( ) . String ( )
clientOrderID = prefix + clientOrderID
if len ( clientOrderID ) > 32 {
return clientOrderID [ 0 : 32 ]
}
return clientOrderID
}
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 ( ) .
Symbol ( order . Symbol ) .
Side ( binance . SideType ( order . Side ) ) .
Type ( orderType )
clientOrderID := newSpotClientOrderID ( order . ClientOrderID )
if len ( clientOrderID ) > 0 {
req . NewClientOrderID ( clientOrderID )
}
if order . Market . Symbol != "" {
req . Quantity ( order . Market . FormatQuantity ( order . Quantity ) )
} else {
// TODO: report error
req . Quantity ( order . Quantity . FormatString ( 8 ) )
}
// set price field for limit orders
switch order . Type {
case types . OrderTypeStopLimit , types . OrderTypeLimit , types . OrderTypeLimitMaker :
if order . Market . Symbol != "" {
req . Price ( order . Market . FormatPrice ( order . Price ) )
} else {
// TODO: report error
req . Price ( order . Price . FormatString ( 8 ) )
}
}
switch order . Type {
case types . OrderTypeStopLimit , types . OrderTypeStopMarket :
if order . Market . Symbol != "" {
req . StopPrice ( order . Market . FormatPrice ( order . StopPrice ) )
} else {
// TODO: report error
req . StopPrice ( order . StopPrice . FormatString ( 8 ) )
}
}
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 )
}
}
req . NewOrderRespType ( binance . NewOrderRespTypeRESULT )
response , err := req . Do ( ctx )
if err != nil {
return nil , err
}
log . Infof ( "spot order creation response: %+v" , response )
createdOrder , err := toGlobalOrder ( & binance . Order {
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 ,
IsIsolated : response . IsIsolated ,
} , false )
return createdOrder , err
}
func ( e * Exchange ) SubmitOrder ( ctx context . Context , order types . SubmitOrder ) ( createdOrder * types . Order , err error ) {
if err = orderLimiter . Wait ( ctx ) ; err != nil {
log . WithError ( err ) . Errorf ( "order rate limiter wait error" )
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 )
}
return createdOrder , err
}
// QueryKLines queries the Kline/candlestick bars for a symbol. Klines are uniquely identified by their open time.
// 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 )
}
var limit = 1000
if options . Limit > 0 {
// default limit == 1000
limit = options . Limit
}
log . Infof ( "querying kline %s %s %v" , symbol , interval , options )
req := e . client . NewKlinesService ( ) .
Symbol ( symbol ) .
Interval ( string ( interval ) ) .
Limit ( limit )
if options . StartTime != nil {
req . StartTime ( options . StartTime . UnixMilli ( ) )
}
if options . EndTime != nil {
req . EndTime ( options . EndTime . UnixMilli ( ) )
}
resp , err := req . Do ( ctx )
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
}
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
// 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 {
req . StartTime ( options . StartTime . UnixMilli ( ) )
} else if options . EndTime != nil {
req . EndTime ( options . EndTime . UnixMilli ( ) )
}
}
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
}
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 ) {
req := e . client2 . NewGetMyTradesRequest ( )
req . Symbol ( symbol )
// BINANCE uses inclusive last trade ID
if options . LastTradeID > 0 {
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 {
req . StartTime ( * options . StartTime )
} else if options . EndTime != nil {
req . EndTime ( * options . EndTime )
}
}
if options . Limit > 0 {
req . Limit ( uint64 ( options . Limit ) )
} else {
req . Limit ( 1000 )
}
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
}
trades = append ( trades , * localTrade )
}
trades = types . SortTradesAscending ( trades )
return trades , nil
}
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
}
if e . IsMargin {
return e . queryMarginTrades ( ctx , symbol , options )
} else if e . IsFutures {
return e . queryFuturesTrades ( ctx , symbol , options )
}
return e . querySpotTrades ( ctx , symbol , options )
}
// DefaultFeeRates returns the Binance VIP 0 fee schedule
// See also https://www.binance.com/en/fee/schedule
// See futures fee at: https://www.binance.com/en/fee/futureFee
func ( e * Exchange ) DefaultFeeRates ( ) types . ExchangeFee {
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 {
MakerFeeRate : fixedpoint . NewFromFloat ( 0.01 * 0.075 ) , // 0.075% with BNB
TakerFeeRate : fixedpoint . NewFromFloat ( 0.01 * 0.075 ) , // 0.075% with BNB
}
}
// 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 . client2 . NewGetDepthRequest ( ) . Symbol ( symbol ) . Limit ( DefaultDepthLimit ) . Do ( ctx )
if err != nil {
return snapshot , finalUpdateID , err
}
return convertDepth ( symbol , response )
}
func convertDepth ( symbol string , response * binanceapi . Depth ) ( snapshot types . SliceOrderBook , finalUpdateID int64 , err error ) {
snapshot . Symbol = symbol
snapshot . Time = time . Now ( )
snapshot . LastUpdateId = response . LastUpdateId
finalUpdateID = response . LastUpdateId
for _ , entry := range response . Bids {
snapshot . Bids = append ( snapshot . Bids , types . PriceVolume { Price : entry [ 0 ] , Volume : entry [ 1 ] } )
}
for _ , entry := range response . Asks {
snapshot . Asks = append ( snapshot . Asks , types . PriceVolume { Price : entry [ 0 ] , Volume : entry [ 1 ] } )
}
return snapshot , finalUpdateID , err
}
func convertDepthLegacy (
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
}
// QueryPremiumIndex is only for futures
func ( e * Exchange ) QueryPremiumIndex ( ctx context . Context , symbol string ) ( * types . PremiumIndex , error ) {
// when symbol is set, only one index will be returned.
indexes , err := e . futuresClient . NewPremiumIndexService ( ) . Symbol ( symbol ) . Do ( ctx )
if err != nil {
return nil , err
}
return convertPremiumIndex ( indexes [ 0 ] )
}
func ( e * Exchange ) QueryFundingRateHistory ( ctx context . Context , symbol string ) ( * types . FundingRate , error ) {
rates , err := e . futuresClient . NewFundingRateService ( ) .
Symbol ( symbol ) .
Limit ( 1 ) .
Do ( ctx )
if err != nil {
return nil , err
}
if len ( rates ) == 0 {
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 {
FundingRate : fundingRate ,
FundingTime : time . Unix ( 0 , rate . FundingTime * int64 ( time . Millisecond ) ) ,
Time : time . Unix ( 0 , rate . FundingTime * int64 ( time . Millisecond ) ) ,
} , nil
}
func ( e * Exchange ) QueryPositionRisk ( ctx context . Context , symbol string ) ( * types . PositionRisk , error ) {
// when symbol is set, only one position risk will be returned.
risks , err := e . futuresClient . NewGetPositionRiskService ( ) . Symbol ( symbol ) . Do ( ctx )
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
}
// 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
}