bbgo/pkg/exchange/binance/convert.go

378 lines
12 KiB
Go
Raw Permalink Normal View History

package binance
import (
"fmt"
"strings"
"time"
"github.com/adshao/go-binance/v2"
"github.com/adshao/go-binance/v2/futures"
"github.com/pkg/errors"
"git.qtrade.icu/lychiyu/bbgo/pkg/exchange/binance/binanceapi"
"git.qtrade.icu/lychiyu/bbgo/pkg/fixedpoint"
"git.qtrade.icu/lychiyu/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 "<symbol>@kline_<interval>"
// for depth, it's "<symbol>@depth OR <symbol>@depth@100ms"
// for trade, it's "<symbol>@trade"
// for aggregated trade, it's "<symbol>@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: <symbol>@depth<levels> OR <symbol>@depth<levels>@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)
}