mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-27 17:25:16 +00:00
332 lines
9.2 KiB
Go
332 lines
9.2 KiB
Go
package bitget
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/c9s/bbgo/pkg/exchange/bitget/bitgetapi"
|
|
v2 "github.com/c9s/bbgo/pkg/exchange/bitget/bitgetapi/v2"
|
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
|
"github.com/c9s/bbgo/pkg/types"
|
|
)
|
|
|
|
func toGlobalSymbol(s string) string {
|
|
return strings.ToUpper(s)
|
|
}
|
|
|
|
func toGlobalBalance(asset bitgetapi.AccountAsset) types.Balance {
|
|
return types.Balance{
|
|
Currency: asset.CoinName,
|
|
Available: asset.Available,
|
|
Locked: asset.Lock.Add(asset.Frozen),
|
|
Borrowed: fixedpoint.Zero,
|
|
Interest: fixedpoint.Zero,
|
|
NetAsset: fixedpoint.Zero,
|
|
MaxWithdrawAmount: fixedpoint.Zero,
|
|
}
|
|
}
|
|
|
|
func toGlobalMarket(s bitgetapi.Symbol) types.Market {
|
|
if s.Status != bitgetapi.SymbolOnline {
|
|
log.Warnf("The symbol %s is not online", s.Symbol)
|
|
}
|
|
return types.Market{
|
|
Symbol: s.SymbolName,
|
|
LocalSymbol: s.Symbol,
|
|
PricePrecision: s.PriceScale.Int(),
|
|
VolumePrecision: s.QuantityScale.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.QuantityScale.Int())),
|
|
TickSize: fixedpoint.NewFromFloat(1.0 / math.Pow10(s.PriceScale.Int())),
|
|
MinPrice: fixedpoint.Zero,
|
|
MaxPrice: fixedpoint.Zero,
|
|
}
|
|
}
|
|
|
|
func toGlobalTicker(ticker bitgetapi.Ticker) types.Ticker {
|
|
return types.Ticker{
|
|
Time: ticker.Ts.Time(),
|
|
Volume: ticker.BaseVol,
|
|
Last: ticker.Close,
|
|
Open: ticker.OpenUtc0,
|
|
High: ticker.High24H,
|
|
Low: ticker.Low24H,
|
|
Buy: ticker.BuyOne,
|
|
Sell: ticker.SellOne,
|
|
}
|
|
}
|
|
|
|
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.CTime),
|
|
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
|
|
|
|
// The market order will be executed immediately, so this check is used to handle corner cases.
|
|
if orderType == types.OrderTypeMarket {
|
|
qty = order.BaseVolume
|
|
log.Warnf("!!! The price(%f) and quantity(%f) are not verified for market orders, because we only receive limit orders in the test environment !!!", price.Float64(), qty.Float64())
|
|
}
|
|
|
|
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.CTime.Time()),
|
|
UpdateTime: types.Time(order.UTime.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.CTime.Time()),
|
|
UpdateTime: types.Time(order.UTime.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:
|
|
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)
|
|
}
|
|
}
|