2024-06-27 14:42:38 +00:00
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 ) .
2024-07-21 14:42:15 +00:00
Side ( futures . SideType ( order . Side ) ) .
PositionSide ( futures . PositionSideType ( order . PositionSide ) )
2024-06-27 14:42:38 +00:00
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 {
2024-07-21 14:42:15 +00:00
case types . OrderTypeStopLimit , types . OrderTypeStopMarket , types . OrderTypeTakeProfitMarket :
2024-06-27 14:42:38 +00:00
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
}