package binance import ( "context" "fmt" "time" "github.com/adshao/go-binance/v2" "github.com/adshao/go-binance/v2/futures" "github.com/google/uuid" "go.uber.org/multierr" "git.qtrade.icu/lychiyu/qbtrade/pkg/exchange/binance/binanceapi" "git.qtrade.icu/lychiyu/qbtrade/pkg/fixedpoint" "git.qtrade.icu/lychiyu/qbtrade/pkg/types" ) func (e *Exchange) queryFuturesClosedOrders( ctx context.Context, symbol string, since, until time.Time, lastOrderID uint64, ) (orders []types.Order, err error) { req := e.futuresClient.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)) } } binanceOrders, err := req.Do(ctx) if err != nil { return orders, err } return toGlobalFuturesOrders(binanceOrders, false) } func (e *Exchange) TransferFuturesAccountAsset( ctx context.Context, asset string, amount fixedpoint.Value, io types.TransferDirection, ) error { req := e.client2.NewFuturesTransferRequest() req.Asset(asset) req.Amount(amount.String()) if io == types.TransferIn { req.TransferType(binanceapi.FuturesTransferSpotToUsdtFutures) } else if io == types.TransferOut { req.TransferType(binanceapi.FuturesTransferUsdtFuturesToSpot) } else { return fmt.Errorf("unexpected transfer direction: %d given", io) } resp, err := req.Do(ctx) switch io { case types.TransferIn: log.Infof("internal transfer (spot) => (futures) %s %s, transaction = %+v, err = %+v", amount.String(), asset, resp, err) case types.TransferOut: log.Infof("internal transfer (futures) => (spot) %s %s, transaction = %+v, err = %+v", amount.String(), asset, resp, err) } return err } // QueryFuturesAccount gets the futures account balances from Binance // Balance.Available = Wallet Balance(in Binance UI) - Used Margin // Balance.Locked = Used Margin func (e *Exchange) QueryFuturesAccount(ctx context.Context) (*types.Account, error) { // account, err := e.futuresClient.NewGetAccountService().Do(ctx) reqAccount := e.futuresClient2.NewFuturesGetAccountRequest() account, err := reqAccount.Do(ctx) if err != nil { return nil, err } req := e.futuresClient2.NewFuturesGetAccountBalanceRequest() accountBalances, err := req.Do(ctx) if err != nil { return nil, err } var balances = map[string]types.Balance{} for _, b := range accountBalances { // The futures account balance is much different from the spot balance: // - Balance is the actual balance of the asset // - AvailableBalance is the available margin balance (can be used as notional) // - CrossWalletBalance (this will be meaningful when using isolated margin) balances[b.Asset] = types.Balance{ Currency: b.Asset, Available: b.AvailableBalance, // AvailableBalance here is the available margin, like how much quantity/notional you can SHORT/LONG, not what you can withdraw Locked: b.Balance.Sub(b.AvailableBalance.Sub(b.CrossUnPnl)), // FIXME: AvailableBalance is the available margin balance, it could be re-calculated by the current formula. MaxWithdrawAmount: b.MaxWithdrawAmount, } } a := &types.Account{ AccountType: types.AccountTypeFutures, FuturesInfo: toGlobalFuturesAccountInfo(account), // In binance GO api, Account define account info which mantain []*AccountAsset and []*AccountPosition. 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) cancelFuturesOrders(ctx context.Context, orders ...types.Order) (err error) { for _, o := range orders { var req = e.futuresClient.NewCancelOrderService() // Mandatory req.Symbol(o.Symbol) if o.OrderID > 0 { req.OrderID(int64(o.OrderID)) } 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) submitFuturesOrder(ctx context.Context, order types.SubmitOrder) (*types.Order, error) { orderType, err := toLocalFuturesOrderType(order.Type) if err != nil { return nil, err } req := e.futuresClient.NewCreateOrderService(). Symbol(order.Symbol). Type(orderType). Side(futures.SideType(order.Side)) if order.ReduceOnly { req.ReduceOnly(order.ReduceOnly) } else if order.ClosePosition { req.ClosePosition(order.ClosePosition) } clientOrderID := newFuturesClientOrderID(order.ClientOrderID) if len(clientOrderID) > 0 { req.NewClientOrderID(clientOrderID) } // use response result format req.NewOrderResponseType(futures.NewOrderRespTypeRESULT) if !order.ClosePosition { 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 if len(order.TimeInForce) > 0 { // TODO: check the TimeInForce value req.TimeInForce(futures.TimeInForceType(order.TimeInForce)) } else { switch order.Type { case types.OrderTypeLimit, types.OrderTypeLimitMaker, types.OrderTypeStopLimit: req.TimeInForce(futures.TimeInForceTypeGTC) } } response, err := req.Do(ctx) if err != nil { return nil, err } log.Infof("futures order creation response: %+v", response) createdOrder, err := toGlobalFuturesOrder(&futures.Order{ Symbol: response.Symbol, OrderID: response.OrderID, ClientOrderID: response.ClientOrderID, Price: response.Price, OrigQuantity: response.OrigQuantity, ExecutedQuantity: response.ExecutedQuantity, Status: response.Status, TimeInForce: response.TimeInForce, Type: response.Type, Side: response.Side, ReduceOnly: response.ReduceOnly, }, false) return createdOrder, err } func (e *Exchange) QueryFuturesKLines( ctx context.Context, symbol string, interval types.Interval, options types.KLineQueryOptions, ) ([]types.KLine, error) { 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.futuresClient.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) queryFuturesTrades( ctx context.Context, symbol string, options *types.TradeQueryOptions, ) (trades []types.Trade, err error) { var remoteTrades []*futures.AccountTrade req := e.futuresClient.NewListAccountTradeService(). Symbol(symbol) if options.Limit > 0 { req.Limit(int(options.Limit)) } else { req.Limit(1000) } // BINANCE uses inclusive last trade ID if options.LastTradeID > 0 { req.FromID(int64(options.LastTradeID)) } // The parameter fromId cannot be sent with startTime or endTime. // Mentioned in binance futures docs if options.LastTradeID <= 0 { 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.EndTime != nil { req.EndTime(options.EndTime.UnixMilli()) } } remoteTrades, err = req.Do(ctx) if err != nil { return nil, err } for _, t := range remoteTrades { localTrade, err := toGlobalFuturesTrade(*t) if err != nil { log.WithError(err).Errorf("can not convert binance futures trade: %+v", t) continue } trades = append(trades, *localTrade) } trades = types.SortTradesAscending(trades) return trades, nil } func (e *Exchange) QueryFuturesPositionRisks(ctx context.Context, symbol string) error { req := e.futuresClient.NewGetPositionRiskService() req.Symbol(symbol) res, err := req.Do(ctx) if err != nil { return err } _ = res return nil } // qbtrade is a futures broker on Binance const futuresBrokerID = "gBhMvywy" func newFuturesClientOrderID(originalID string) (clientOrderID string) { if originalID == types.NoClientOrderID { return "" } prefix := "x-" + futuresBrokerID 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) queryFuturesDepth(ctx context.Context, symbol string) (snapshot types.SliceOrderBook, finalUpdateID int64, err error) { res, err := e.futuresClient.NewDepthService().Symbol(symbol).Do(ctx) if err != nil { return snapshot, finalUpdateID, err } response := &binance.DepthResponse{ LastUpdateID: res.LastUpdateID, Bids: res.Bids, Asks: res.Asks, } return convertDepthLegacy(snapshot, symbol, finalUpdateID, response) } func (e *Exchange) GetFuturesClient() *binanceapi.FuturesRestClient { return e.futuresClient2 } // QueryFuturesIncomeHistory queries the income history on the binance futures account // This is more binance futures specific API, the convert function is not designed yet. // TODO: consider other futures platforms and design the common data structure for this func (e *Exchange) QueryFuturesIncomeHistory( ctx context.Context, symbol string, incomeType binanceapi.FuturesIncomeType, startTime, endTime *time.Time, ) ([]binanceapi.FuturesIncome, error) { req := e.futuresClient2.NewFuturesGetIncomeHistoryRequest() req.Symbol(symbol) req.IncomeType(incomeType) if startTime != nil { req.StartTime(*startTime) } if endTime != nil { req.EndTime(*endTime) } resp, err := req.Do(ctx) return resp, err }