bbgo/pkg/exchange/bitget/convert.go

498 lines
14 KiB
Go

package bitget
import (
"errors"
"fmt"
"math"
"strconv"
"time"
v2 "git.qtrade.icu/lychiyu/bbgo/pkg/exchange/bitget/bitgetapi/v2"
"git.qtrade.icu/lychiyu/bbgo/pkg/fixedpoint"
"git.qtrade.icu/lychiyu/bbgo/pkg/types"
)
func toGlobalBalance(asset v2.AccountAsset) types.Balance {
return types.Balance{
Currency: asset.Coin,
Available: asset.Available,
Locked: asset.Locked.Add(asset.Frozen),
Borrowed: fixedpoint.Zero,
Interest: fixedpoint.Zero,
NetAsset: fixedpoint.Zero,
MaxWithdrawAmount: fixedpoint.Zero,
}
}
func toGlobalMarket(s v2.Symbol) types.Market {
if s.Status != v2.SymbolStatusOnline {
log.Warnf("The market symbol status %s is not online: %s", s.Symbol, s.Status)
}
return types.Market{
Exchange: types.ExchangeBitget,
Symbol: s.Symbol,
LocalSymbol: s.Symbol,
PricePrecision: s.PricePrecision.Int(),
VolumePrecision: s.QuantityPrecision.Int(),
QuoteCurrency: s.QuoteCoin,
BaseCurrency: s.BaseCoin,
MinNotional: s.MinTradeUSDT,
MinAmount: s.MinTradeUSDT,
MinQuantity: s.MinTradeAmount,
MaxQuantity: s.MaxTradeAmount,
StepSize: fixedpoint.NewFromFloat(1.0 / math.Pow10(s.QuantityPrecision.Int())),
TickSize: fixedpoint.NewFromFloat(1.0 / math.Pow10(s.PricePrecision.Int())),
MinPrice: fixedpoint.Zero,
MaxPrice: fixedpoint.Zero,
}
}
func toGlobalTicker(ticker v2.Ticker) types.Ticker {
return types.Ticker{
Time: ticker.Ts.Time(),
Volume: ticker.BaseVolume,
Last: ticker.LastPr,
Open: ticker.Open,
High: ticker.High24H,
Low: ticker.Low24H,
Buy: ticker.BidPr,
Sell: ticker.AskPr,
}
}
func toGlobalSideType(side v2.SideType) (types.SideType, error) {
switch side {
case v2.SideTypeBuy:
return types.SideTypeBuy, nil
case v2.SideTypeSell:
return types.SideTypeSell, nil
default:
return types.SideType(side), fmt.Errorf("unexpected side: %s", side)
}
}
func toGlobalOrderType(s v2.OrderType) (types.OrderType, error) {
switch s {
case v2.OrderTypeMarket:
return types.OrderTypeMarket, nil
case v2.OrderTypeLimit:
return types.OrderTypeLimit, nil
default:
return types.OrderType(s), fmt.Errorf("unexpected order type: %s", s)
}
}
func toGlobalOrderStatus(status v2.OrderStatus) (types.OrderStatus, error) {
switch status {
case v2.OrderStatusInit, v2.OrderStatusNew, v2.OrderStatusLive:
return types.OrderStatusNew, nil
case v2.OrderStatusPartialFilled:
return types.OrderStatusPartiallyFilled, nil
case v2.OrderStatusFilled:
return types.OrderStatusFilled, nil
case v2.OrderStatusCancelled:
return types.OrderStatusCanceled, nil
default:
return types.OrderStatus(status), fmt.Errorf("unexpected order status: %s", status)
}
}
func isMaker(s v2.TradeScope) (bool, error) {
switch s {
case v2.TradeMaker:
return true, nil
case v2.TradeTaker:
return false, nil
default:
return false, fmt.Errorf("unexpected trade scope: %s", s)
}
}
func isFeeDiscount(s v2.DiscountStatus) (bool, error) {
switch s {
case v2.DiscountYes:
return true, nil
case v2.DiscountNo:
return false, nil
default:
return false, fmt.Errorf("unexpected discount status: %s", s)
}
}
func toGlobalTrade(trade v2.Trade) (*types.Trade, error) {
side, err := toGlobalSideType(trade.Side)
if err != nil {
return nil, err
}
isMaker, err := isMaker(trade.TradeScope)
if err != nil {
return nil, err
}
isDiscount, err := isFeeDiscount(trade.FeeDetail.Deduction)
if err != nil {
return nil, err
}
return &types.Trade{
ID: uint64(trade.TradeId),
OrderID: uint64(trade.OrderId),
Exchange: types.ExchangeBitget,
Price: trade.PriceAvg,
Quantity: trade.Size,
QuoteQuantity: trade.Amount,
Symbol: trade.Symbol,
Side: side,
IsBuyer: side == types.SideTypeBuy,
IsMaker: isMaker,
Time: types.Time(trade.CreatedTime),
Fee: trade.FeeDetail.TotalFee.Abs(),
FeeCurrency: trade.FeeDetail.FeeCoin,
FeeDiscounted: isDiscount,
}, nil
}
// unfilledOrderToGlobalOrder convert the local order to global.
//
// Note that the quantity unit, according official document: Base coin when orderType=limit; Quote coin when orderType=market
// https://bitgetlimited.github.io/apidoc/zh/spot/#19671a1099
func unfilledOrderToGlobalOrder(order v2.UnfilledOrder) (*types.Order, error) {
side, err := toGlobalSideType(order.Side)
if err != nil {
return nil, err
}
orderType, err := toGlobalOrderType(order.OrderType)
if err != nil {
return nil, err
}
status, err := toGlobalOrderStatus(order.Status)
if err != nil {
return nil, err
}
qty := order.Size
price := order.PriceAvg
// 2023/11/05 The market order will be executed immediately, so this check is used to handle corner cases.
// 2024/03/06 After placing a Market Order, we can retrieve it through the unfilledOrder API, so we still need to
// handle the Market Order status.
if orderType == types.OrderTypeMarket {
price = order.PriceAvg
if side == types.SideTypeBuy {
qty, err = processMarketBuyQuantity(order.BaseVolume, order.QuoteVolume, order.PriceAvg, order.Size, order.Status)
if err != nil {
return nil, err
}
}
}
return &types.Order{
SubmitOrder: types.SubmitOrder{
ClientOrderID: order.ClientOrderId,
Symbol: order.Symbol,
Side: side,
Type: orderType,
Quantity: qty,
Price: price,
// Bitget does not include the "time-in-force" field in its API response for spot trading, so we set GTC.
TimeInForce: types.TimeInForceGTC,
},
Exchange: types.ExchangeBitget,
OrderID: uint64(order.OrderId),
UUID: strconv.FormatInt(int64(order.OrderId), 10),
Status: status,
ExecutedQuantity: order.BaseVolume,
IsWorking: order.Status.IsWorking(),
CreationTime: types.Time(order.CreatedTime.Time()),
UpdateTime: types.Time(order.UpdatedTime.Time()),
}, nil
}
func toGlobalOrder(order v2.OrderDetail) (*types.Order, error) {
side, err := toGlobalSideType(order.Side)
if err != nil {
return nil, err
}
orderType, err := toGlobalOrderType(order.OrderType)
if err != nil {
return nil, err
}
status, err := toGlobalOrderStatus(order.Status)
if err != nil {
return nil, err
}
qty := order.Size
price := order.Price
if orderType == types.OrderTypeMarket {
price = order.PriceAvg
if side == types.SideTypeBuy {
qty, err = processMarketBuyQuantity(order.BaseVolume, order.QuoteVolume, order.PriceAvg, order.Size, order.Status)
if err != nil {
return nil, err
}
}
}
return &types.Order{
SubmitOrder: types.SubmitOrder{
ClientOrderID: order.ClientOrderId,
Symbol: order.Symbol,
Side: side,
Type: orderType,
Quantity: qty,
Price: price,
// Bitget does not include the "time-in-force" field in its API response for spot trading, so we set GTC.
TimeInForce: types.TimeInForceGTC,
},
Exchange: types.ExchangeBitget,
OrderID: uint64(order.OrderId),
UUID: strconv.FormatInt(int64(order.OrderId), 10),
Status: status,
ExecutedQuantity: order.BaseVolume,
IsWorking: order.Status.IsWorking(),
CreationTime: types.Time(order.CreatedTime.Time()),
UpdateTime: types.Time(order.UpdatedTime.Time()),
}, nil
}
// processMarketBuyQuantity returns the estimated base quantity or real. The order size will be 'quote quantity' when side is buy and
// type is market, so we need to convert that. This is because the unit of types.Order.Quantity is base coin.
//
// If the order status is PartialFilled, return estimated base coin quantity.
// If the order status is Filled, return the filled base quantity instead of the buy quantity, because a market order on the buy side
// cannot execute all.
// Otherwise, return zero.
func processMarketBuyQuantity(
filledQty, filledPrice, priceAvg, buyQty fixedpoint.Value, orderStatus v2.OrderStatus,
) (fixedpoint.Value, error) {
switch orderStatus {
case v2.OrderStatusInit, v2.OrderStatusNew, v2.OrderStatusLive, v2.OrderStatusCancelled:
return fixedpoint.Zero, nil
case v2.OrderStatusPartialFilled:
// sanity check for avoid divide 0
if priceAvg.IsZero() {
return fixedpoint.Zero, errors.New("priceAvg for a partialFilled should not be zero")
}
// calculate the remaining quote coin quantity.
remainPrice := buyQty.Sub(filledPrice)
// calculate the remaining base coin quantity.
remainBaseCoinQty := remainPrice.Div(priceAvg)
// Estimated quantity that may be purchased.
return filledQty.Add(remainBaseCoinQty), nil
case v2.OrderStatusFilled:
// Market buy orders may not purchase the entire quantity, hence the use of filledQty here.
return filledQty, nil
default:
return fixedpoint.Zero, fmt.Errorf("failed to execute market buy quantity due to unexpected order status %s ", orderStatus)
}
}
func toLocalOrderType(orderType types.OrderType) (v2.OrderType, error) {
switch orderType {
case types.OrderTypeLimit, types.OrderTypeLimitMaker:
return v2.OrderTypeLimit, nil
case types.OrderTypeMarket:
return v2.OrderTypeMarket, nil
default:
return "", fmt.Errorf("order type %s not supported", orderType)
}
}
func toLocalSide(side types.SideType) (v2.SideType, error) {
switch side {
case types.SideTypeSell:
return v2.SideTypeSell, nil
case types.SideTypeBuy:
return v2.SideTypeBuy, nil
default:
return "", fmt.Errorf("side type %s not supported", side)
}
}
func toGlobalBalanceMap(balances []Balance) types.BalanceMap {
bm := types.BalanceMap{}
for _, obj := range balances {
bm[obj.Coin] = types.Balance{
Currency: obj.Coin,
Available: obj.Available,
Locked: obj.Frozen.Add(obj.Locked),
}
}
return bm
}
func toGlobalKLines(symbol string, interval types.Interval, kLines v2.KLineResponse) []types.KLine {
gKLines := make([]types.KLine, len(kLines))
for i, kline := range kLines {
// follow the binance rule, to avoid endTime overlapping with the next startTime. So we subtract -1 time.Millisecond
// on endTime.
endTime := types.Time(kline.Ts.Time().Add(interval.Duration() - time.Millisecond))
gKLines[i] = types.KLine{
Exchange: types.ExchangeBitget,
Symbol: symbol,
StartTime: types.Time(kline.Ts),
EndTime: endTime,
Interval: interval,
Open: kline.Open,
Close: kline.Close,
High: kline.High,
Low: kline.Low,
Volume: kline.Volume,
QuoteVolume: kline.QuoteVolume,
// Bitget doesn't support close flag in REST API
Closed: false,
}
}
return gKLines
}
func toGlobalTimeInForce(force v2.OrderForce) (types.TimeInForce, error) {
switch force {
case v2.OrderForceFOK:
return types.TimeInForceFOK, nil
case v2.OrderForceGTC, v2.OrderForcePostOnly:
return types.TimeInForceGTC, nil
case v2.OrderForceIOC:
return types.TimeInForceIOC, nil
default:
return "", fmt.Errorf("unexpected time-in-force: %s", force)
}
}
func (o *Order) processMarketBuyQuantity() (fixedpoint.Value, error) {
switch o.Status {
case v2.OrderStatusLive, v2.OrderStatusNew, v2.OrderStatusInit, v2.OrderStatusCancelled:
return fixedpoint.Zero, nil
case v2.OrderStatusPartialFilled:
if o.FillPrice.IsZero() {
return fixedpoint.Zero, fmt.Errorf("fillPrice for a partialFilled should not be zero")
}
return o.NewSize.Div(o.FillPrice), nil
case v2.OrderStatusFilled:
return o.AccBaseVolume, nil
default:
return fixedpoint.Zero, fmt.Errorf("unexpected status: %s", o.Status)
}
}
func (o *Order) toGlobalOrder() (types.Order, error) {
side, err := toGlobalSideType(o.Side)
if err != nil {
return types.Order{}, err
}
orderType, err := toGlobalOrderType(o.OrderType)
if err != nil {
return types.Order{}, err
}
timeInForce, err := toGlobalTimeInForce(o.Force)
if err != nil {
return types.Order{}, err
}
status, err := toGlobalOrderStatus(o.Status)
if err != nil {
return types.Order{}, err
}
qty := o.NewSize
if orderType == types.OrderTypeMarket && side == types.SideTypeBuy {
qty, err = o.processMarketBuyQuantity()
if err != nil {
return types.Order{}, err
}
}
price := o.Price
if orderType == types.OrderTypeMarket {
price = o.PriceAvg
}
return types.Order{
SubmitOrder: types.SubmitOrder{
ClientOrderID: o.ClientOrderId,
Symbol: o.InstId,
Side: side,
Type: orderType,
Quantity: qty,
Price: price,
TimeInForce: timeInForce,
},
Exchange: types.ExchangeBitget,
OrderID: uint64(o.OrderId),
UUID: strconv.FormatInt(int64(o.OrderId), 10),
Status: status,
ExecutedQuantity: o.AccBaseVolume,
IsWorking: o.Status.IsWorking(),
CreationTime: types.Time(o.CreatedTime.Time()),
UpdateTime: types.Time(o.UpdatedTime.Time()),
}, nil
}
func (o *Order) toGlobalTrade() (types.Trade, error) {
if o.Status != v2.OrderStatusPartialFilled {
return types.Trade{}, fmt.Errorf("failed to convert to global trade, unexpected status: %s", o.Status)
}
side, err := toGlobalSideType(o.Side)
if err != nil {
return types.Trade{}, err
}
isMaker, err := o.isMaker()
if err != nil {
return types.Trade{}, err
}
return types.Trade{
ID: uint64(o.TradeId),
OrderID: uint64(o.OrderId),
Exchange: types.ExchangeBitget,
Price: o.FillPrice,
Quantity: o.BaseVolume,
QuoteQuantity: o.FillPrice.Mul(o.BaseVolume),
Symbol: o.InstId,
Side: side,
IsBuyer: side == types.SideTypeBuy,
IsMaker: isMaker,
Time: types.Time(o.FillTime),
Fee: o.FillFee.Abs(),
FeeCurrency: o.FillFeeCoin,
}, nil
}