package binance import ( "fmt" "strings" "time" "github.com/adshao/go-binance/v2" "github.com/adshao/go-binance/v2/futures" "github.com/pkg/errors" "github.com/c9s/bbgo/pkg/exchange/binance/binanceapi" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" ) func toGlobalWithdrawStatus(status binanceapi.WithdrawStatus) (types.WithdrawStatus, error) { switch status { case binanceapi.WithdrawStatusEmailSent: return types.WithdrawStatusSent, nil case binanceapi.WithdrawStatusCancelled: return types.WithdrawStatusCancelled, nil case binanceapi.WithdrawStatusAwaitingApproval: return types.WithdrawStatusAwaitingApproval, nil case binanceapi.WithdrawStatusRejected: return types.WithdrawStatusRejected, nil case binanceapi.WithdrawStatusProcessing: return types.WithdrawStatusProcessing, nil case binanceapi.WithdrawStatusFailure: return types.WithdrawStatusFailed, nil case binanceapi.WithdrawStatusCompleted: return types.WithdrawStatusCompleted, nil default: return types.WithdrawStatusUnknown, fmt.Errorf("unable to convert the withdraw status: %s", status) } } func toGlobalMarket(symbol binance.Symbol) types.Market { market := types.Market{ Exchange: types.ExchangeBinance, Symbol: symbol.Symbol, LocalSymbol: symbol.Symbol, PricePrecision: symbol.QuotePrecision, VolumePrecision: symbol.BaseAssetPrecision, QuoteCurrency: symbol.QuoteAsset, BaseCurrency: symbol.BaseAsset, } if f := symbol.NotionalFilter(); f != nil { market.MinNotional = fixedpoint.MustNewFromString(f.MinNotional) market.MinAmount = fixedpoint.MustNewFromString(f.MinNotional) } // The LOT_SIZE filter defines the quantity (aka "lots" in auction terms) rules for a symbol. // There are 3 parts: // minQty defines the minimum quantity/icebergQty allowed. // maxQty defines the maximum quantity/icebergQty allowed. // stepSize defines the intervals that a quantity/icebergQty can be increased/decreased by. if f := symbol.LotSizeFilter(); f != nil { market.MinQuantity = fixedpoint.MustNewFromString(f.MinQuantity) market.MaxQuantity = fixedpoint.MustNewFromString(f.MaxQuantity) market.StepSize = fixedpoint.MustNewFromString(f.StepSize) } if f := symbol.PriceFilter(); f != nil { market.MaxPrice = fixedpoint.MustNewFromString(f.MaxPrice) market.MinPrice = fixedpoint.MustNewFromString(f.MinPrice) market.TickSize = fixedpoint.MustNewFromString(f.TickSize) } if market.MinNotional.IsZero() { log.Warnf("binance market %s minNotional is zero", market.Symbol) } if market.MinQuantity.IsZero() { log.Warnf("binance market %s minQuantity is zero", market.Symbol) } return market } // TODO: Cuz it returns types.Market as well, merge following to the above function func toGlobalFuturesMarket(symbol futures.Symbol) types.Market { market := types.Market{ Exchange: types.ExchangeBinance, Symbol: symbol.Symbol, LocalSymbol: symbol.Symbol, PricePrecision: symbol.QuotePrecision, VolumePrecision: symbol.BaseAssetPrecision, QuoteCurrency: symbol.QuoteAsset, BaseCurrency: symbol.BaseAsset, } if f := symbol.MinNotionalFilter(); f != nil { market.MinNotional = fixedpoint.MustNewFromString(f.Notional) market.MinAmount = fixedpoint.MustNewFromString(f.Notional) } // The LOT_SIZE filter defines the quantity (aka "lots" in auction terms) rules for a symbol. // There are 3 parts: // minQty defines the minimum quantity/icebergQty allowed. // maxQty defines the maximum quantity/icebergQty allowed. // stepSize defines the intervals that a quantity/icebergQty can be increased/decreased by. if f := symbol.LotSizeFilter(); f != nil { market.MinQuantity = fixedpoint.MustNewFromString(f.MinQuantity) market.MaxQuantity = fixedpoint.MustNewFromString(f.MaxQuantity) market.StepSize = fixedpoint.MustNewFromString(f.StepSize) } if f := symbol.PriceFilter(); f != nil { market.MaxPrice = fixedpoint.MustNewFromString(f.MaxPrice) market.MinPrice = fixedpoint.MustNewFromString(f.MinPrice) market.TickSize = fixedpoint.MustNewFromString(f.TickSize) } return market } // func toGlobalIsolatedMarginAccount(account *binance.IsolatedMarginAccount) *types.IsolatedMarginAccount { // return &types.IsolatedMarginAccount{ // TotalAssetOfBTC: fixedpoint.MustNewFromString(account.TotalNetAssetOfBTC), // TotalLiabilityOfBTC: fixedpoint.MustNewFromString(account.TotalLiabilityOfBTC), // TotalNetAssetOfBTC: fixedpoint.MustNewFromString(account.TotalNetAssetOfBTC), // Assets: toGlobalIsolatedMarginAssets(account.Assets), // } // } func toGlobalTicker(stats *binance.PriceChangeStats) (*types.Ticker, error) { return &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)), }, nil } func toGlobalFuturesTicker(stats *futures.PriceChangeStats) (*types.Ticker, error) { return &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)), }, nil } func toLocalOrderType(orderType types.OrderType) (binance.OrderType, error) { switch orderType { case types.OrderTypeLimitMaker: return binance.OrderTypeLimitMaker, nil case types.OrderTypeLimit: return binance.OrderTypeLimit, nil case types.OrderTypeStopLimit: return binance.OrderTypeStopLossLimit, nil case types.OrderTypeStopMarket: return binance.OrderTypeStopLoss, nil case types.OrderTypeMarket: return binance.OrderTypeMarket, nil } return "", fmt.Errorf("can not convert to local order, order type %s not supported", orderType) } func toGlobalOrders(binanceOrders []*binance.Order, isMargin bool) (orders []types.Order, err error) { for _, binanceOrder := range binanceOrders { order, err := toGlobalOrder(binanceOrder, isMargin) if err != nil { return orders, err } orders = append(orders, *order) } return orders, err } func toGlobalOrder(binanceOrder *binance.Order, isMargin bool) (*types.Order, error) { return &types.Order{ SubmitOrder: types.SubmitOrder{ ClientOrderID: binanceOrder.ClientOrderID, Symbol: binanceOrder.Symbol, Side: toGlobalSideType(binanceOrder.Side), Type: toGlobalOrderType(binanceOrder.Type), Quantity: fixedpoint.MustNewFromString(binanceOrder.OrigQuantity), Price: fixedpoint.MustNewFromString(binanceOrder.Price), TimeInForce: types.TimeInForce(binanceOrder.TimeInForce), }, Exchange: types.ExchangeBinance, IsWorking: binanceOrder.IsWorking, OrderID: uint64(binanceOrder.OrderID), Status: toGlobalOrderStatus(binanceOrder.Status), OriginalStatus: string(binanceOrder.Status), ExecutedQuantity: fixedpoint.MustNewFromString(binanceOrder.ExecutedQuantity), CreationTime: types.Time(millisecondTime(binanceOrder.Time)), UpdateTime: types.Time(millisecondTime(binanceOrder.UpdateTime)), IsMargin: isMargin, IsIsolated: binanceOrder.IsIsolated, }, nil } func millisecondTime(t int64) time.Time { return time.Unix(0, t*int64(time.Millisecond)) } func toGlobalTrade(t binance.TradeV3, isMargin bool) (*types.Trade, error) { // skip trade ID that is the same. however this should not happen var side types.SideType if t.IsBuyer { side = types.SideTypeBuy } else { side = types.SideTypeSell } price, err := fixedpoint.NewFromString(t.Price) if err != nil { return nil, errors.Wrapf(err, "price parse error, price: %+v", t.Price) } quantity, err := fixedpoint.NewFromString(t.Quantity) if err != nil { return nil, errors.Wrapf(err, "quantity parse error, quantity: %+v", t.Quantity) } var quoteQuantity fixedpoint.Value if len(t.QuoteQuantity) > 0 { quoteQuantity, err = fixedpoint.NewFromString(t.QuoteQuantity) if err != nil { return nil, errors.Wrapf(err, "quote quantity parse error, quoteQuantity: %+v", t.QuoteQuantity) } } else { quoteQuantity = price.Mul(quantity) } fee, err := fixedpoint.NewFromString(t.Commission) if err != nil { return nil, errors.Wrapf(err, "commission parse error, commission: %+v", t.Commission) } return &types.Trade{ ID: uint64(t.ID), OrderID: uint64(t.OrderID), Price: price, Symbol: t.Symbol, Exchange: types.ExchangeBinance, Quantity: quantity, QuoteQuantity: quoteQuantity, Side: side, IsBuyer: t.IsBuyer, IsMaker: t.IsMaker, Fee: fee, FeeCurrency: t.CommissionAsset, Time: types.Time(millisecondTime(t.Time)), IsMargin: isMargin, IsIsolated: t.IsIsolated, }, nil } func toGlobalSideType(side binance.SideType) types.SideType { switch side { case binance.SideTypeBuy: return types.SideTypeBuy case binance.SideTypeSell: return types.SideTypeSell default: log.Errorf("can not convert binance side type, unknown side type: %q", side) return "" } } func toGlobalOrderType(orderType binance.OrderType) types.OrderType { switch orderType { case binance.OrderTypeLimit, binance.OrderTypeLimitMaker, binance.OrderTypeTakeProfitLimit: return types.OrderTypeLimit case binance.OrderTypeMarket: return types.OrderTypeMarket case binance.OrderTypeStopLossLimit: return types.OrderTypeStopLimit case binance.OrderTypeStopLoss: return types.OrderTypeStopMarket default: log.Errorf("unsupported order type: %v", orderType) return "" } } func toGlobalOrderStatus(orderStatus binance.OrderStatusType) types.OrderStatus { switch orderStatus { case binance.OrderStatusTypeNew: return types.OrderStatusNew case binance.OrderStatusTypeRejected: return types.OrderStatusRejected case binance.OrderStatusTypeCanceled, binance.OrderStatusTypeExpired, binance.OrderStatusTypePendingCancel: return types.OrderStatusCanceled case binance.OrderStatusTypePartiallyFilled: return types.OrderStatusPartiallyFilled case binance.OrderStatusTypeFilled: return types.OrderStatusFilled } return types.OrderStatus(orderStatus) } func convertSubscription(s types.Subscription) string { // binance uses lower case symbol name, // for kline, it's "@kline_" // for depth, it's "@depth OR @depth@100ms" // for trade, it's "@trade" // for aggregated trade, it's "@aggTrade" switch s.Channel { case types.KLineChannel: return fmt.Sprintf("%s@%s_%s", strings.ToLower(s.Symbol), s.Channel, s.Options.String()) case types.BookChannel: // depth values: 5, 10, 20 // Stream Names: @depth OR @depth@100ms. // Update speed: 1000ms or 100ms n := strings.ToLower(s.Symbol) + "@depth" switch s.Options.Depth { case types.DepthLevel5: n += "5" case types.DepthLevel10: n += "10" case types.DepthLevel20, types.DepthLevelMedium: n += "20" // default to full case types.DepthLevelFull: default: } switch s.Options.Speed { case types.SpeedHigh: n += "@100ms" case types.SpeedLow: n += "@1000ms" } return n case types.BookTickerChannel: return fmt.Sprintf("%s@bookTicker", strings.ToLower(s.Symbol)) case types.MarketTradeChannel: return fmt.Sprintf("%s@trade", strings.ToLower(s.Symbol)) case types.AggTradeChannel: return fmt.Sprintf("%s@aggTrade", strings.ToLower(s.Symbol)) case types.ForceOrderChannel: return fmt.Sprintf("%s@forceOrder", strings.ToLower(s.Symbol)) } return fmt.Sprintf("%s@%s", strings.ToLower(s.Symbol), s.Channel) }