diff --git a/pkg/exchange/bitget/convert.go b/pkg/exchange/bitget/convert.go index 3a8a875dc..4098e8910 100644 --- a/pkg/exchange/bitget/convert.go +++ b/pkg/exchange/bitget/convert.go @@ -362,3 +362,120 @@ func toGlobalKLines(symbol string, interval types.Interval, kLines v2.KLineRespo } 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.Size.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.Size + if orderType == types.OrderTypeMarket && side == types.SideTypeBuy { + qty, err = o.processMarketBuyQuantity() + if err != nil { + return types.Order{}, err + } + } + + return types.Order{ + SubmitOrder: types.SubmitOrder{ + ClientOrderID: o.ClientOrderId, + Symbol: o.InstId, + Side: side, + Type: orderType, + Quantity: qty, + Price: o.PriceAvg, + 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.CTime.Time()), + UpdateTime: types.Time(o.UTime.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 +} diff --git a/pkg/exchange/bitget/convert_test.go b/pkg/exchange/bitget/convert_test.go index dff77b436..57b5366fd 100644 --- a/pkg/exchange/bitget/convert_test.go +++ b/pkg/exchange/bitget/convert_test.go @@ -686,3 +686,452 @@ func Test_toGlobalKLines(t *testing.T) { assert.Equal(t, toGlobalKLines(symbol, interval, resp), expKlines) } + +func Test_toGlobalTimeInForce(t *testing.T) { + force, err := toGlobalTimeInForce(v2.OrderForceFOK) + assert.NoError(t, err) + assert.Equal(t, types.TimeInForceFOK, force) + + force, err = toGlobalTimeInForce(v2.OrderForceGTC) + assert.NoError(t, err) + assert.Equal(t, types.TimeInForceGTC, force) + + force, err = toGlobalTimeInForce(v2.OrderForcePostOnly) + assert.NoError(t, err) + assert.Equal(t, types.TimeInForceGTC, force) + + force, err = toGlobalTimeInForce(v2.OrderForceIOC) + assert.NoError(t, err) + assert.Equal(t, types.TimeInForceIOC, force) + + _, err = toGlobalTimeInForce("xxx") + assert.ErrorContains(t, err, "xxx") +} + +func TestOrder_processMarketBuyQuantity(t *testing.T) { + t.Run("zero qty", func(t *testing.T) { + o := Order{} + for _, s := range []v2.OrderStatus{v2.OrderStatusLive, v2.OrderStatusNew, v2.OrderStatusInit, v2.OrderStatusCancelled} { + o.Status = s + qty, err := o.processMarketBuyQuantity() + assert.NoError(t, err) + assert.Equal(t, fixedpoint.Zero, qty) + } + }) + + t.Run("calculate qty", func(t *testing.T) { + o := Order{ + Size: fixedpoint.NewFromFloat(2), + Trade: Trade{ + FillPrice: fixedpoint.NewFromFloat(1), + }, + Status: v2.OrderStatusPartialFilled, + } + qty, err := o.processMarketBuyQuantity() + assert.NoError(t, err) + assert.Equal(t, fixedpoint.NewFromFloat(2), qty) + }) + + t.Run("return accumulated balance", func(t *testing.T) { + o := Order{ + AccBaseVolume: fixedpoint.NewFromFloat(5), + Status: v2.OrderStatusFilled, + } + qty, err := o.processMarketBuyQuantity() + assert.NoError(t, err) + assert.Equal(t, fixedpoint.NewFromFloat(5), qty) + }) + + t.Run("unexpected status", func(t *testing.T) { + o := Order{ + Status: "xxx", + } + _, err := o.processMarketBuyQuantity() + assert.ErrorContains(t, err, "xxx") + }) +} + +func TestOrder_toGlobalOrder(t *testing.T) { + o := Order{ + Trade: Trade{ + FillPrice: fixedpoint.NewFromFloat(0.49016), + TradeId: types.StrInt64(1107950490073112582), + BaseVolume: fixedpoint.NewFromFloat(33.6558), + FillTime: types.NewMillisecondTimestampFromInt(1699881902235), + FillFee: fixedpoint.NewFromFloat(-0.0336558), + FillFeeCoin: "BGB", + TradeScope: "T", + }, + InstId: "BGBUSDT", + OrderId: types.StrInt64(1107950489998626816), + ClientOrderId: "cc73aab9-1e44-4022-8458-60d8c6a08753", + Size: fixedpoint.NewFromFloat(39.0), + Notional: fixedpoint.NewFromFloat(39.0), + OrderType: v2.OrderTypeMarket, + Force: v2.OrderForceGTC, + Side: v2.SideTypeBuy, + AccBaseVolume: fixedpoint.NewFromFloat(33.6558), + PriceAvg: fixedpoint.NewFromFloat(0.49016), + Status: v2.OrderStatusPartialFilled, + CTime: types.NewMillisecondTimestampFromInt(1699881902217), + UTime: types.NewMillisecondTimestampFromInt(1699881902248), + FeeDetail: nil, + EnterPointSource: "API", + } + + // market buy example: + // { + // "instId":"BGBUSDT", + // "orderId":"1107950489998626816", + // "clientOid":"cc73aab9-1e44-4022-8458-60d8c6a08753", + // "size":"39.0000", + // "notional":"39.000000", + // "orderType":"market", + // "force":"gtc", + // "side":"buy", + // "fillPrice":"0.49016", + // "tradeId":"1107950490073112582", + // "baseVolume":"33.6558", + // "fillTime":"1699881902235", + // "fillFee":"-0.0336558", + // "fillFeeCoin":"BGB", + // "tradeScope":"T", + // "accBaseVolume":"33.6558", + // "priceAvg":"0.49016", + // "status":"partially_filled", + // "cTime":"1699881902217", + // "uTime":"1699881902248", + // "feeDetail":[ + // { + // "feeCoin":"BGB", + // "fee":"-0.0336558" + // } + // ], + // "enterPointSource":"API" + // } + t.Run("market buy", func(t *testing.T) { + newO := o + res, err := newO.toGlobalOrder() + assert.NoError(t, err) + assert.Equal(t, types.Order{ + SubmitOrder: types.SubmitOrder{ + ClientOrderID: "cc73aab9-1e44-4022-8458-60d8c6a08753", + Symbol: "BGBUSDT", + Side: types.SideTypeBuy, + Type: types.OrderTypeMarket, + Quantity: newO.Size.Div(newO.FillPrice), + Price: newO.PriceAvg, + TimeInForce: types.TimeInForceGTC, + }, + Exchange: types.ExchangeBitget, + OrderID: uint64(newO.OrderId), + UUID: strconv.FormatInt(int64(newO.OrderId), 10), + Status: types.OrderStatusPartiallyFilled, + ExecutedQuantity: newO.AccBaseVolume, + IsWorking: newO.Status.IsWorking(), + CreationTime: types.Time(newO.CTime), + UpdateTime: types.Time(newO.UTime), + }, res) + }) + + // market sell example: + // { + // "instId":"BGBUSDT", + // "orderId":"1107940456212631553", + // "clientOid":"088bb971-858e-48e2-b503-85c3274edd89", + // "size":"285.0000", + // "orderType":"market", + // "force":"gtc", + // "side":"sell", + // "fillPrice":"0.48706", + // "tradeId":"1107940456278728706", + // "baseVolume":"22.5840", + // "fillTime":"1699879509992", + // "fillFee":"-0.01099976304", + // "fillFeeCoin":"USDT", + // "tradeScope":"T", + // "accBaseVolume":"45.1675", + // "priceAvg":"0.48706", + // "status":"partially_filled", + // "cTime":"1699879509976", + // "uTime":"1699879510007", + // "feeDetail":[ + // { + // "feeCoin":"USDT", + // "fee":"-0.02199928255" + // } + // ], + // "enterPointSource":"API" + // } + t.Run("market sell", func(t *testing.T) { + newO := o + newO.OrderType = v2.OrderTypeMarket + newO.Side = v2.SideTypeSell + + res, err := newO.toGlobalOrder() + assert.NoError(t, err) + assert.Equal(t, types.Order{ + SubmitOrder: types.SubmitOrder{ + ClientOrderID: "cc73aab9-1e44-4022-8458-60d8c6a08753", + Symbol: "BGBUSDT", + Side: types.SideTypeSell, + Type: types.OrderTypeMarket, + Quantity: newO.Size, + Price: newO.PriceAvg, + TimeInForce: types.TimeInForceGTC, + }, + Exchange: types.ExchangeBitget, + OrderID: uint64(newO.OrderId), + UUID: strconv.FormatInt(int64(newO.OrderId), 10), + Status: types.OrderStatusPartiallyFilled, + ExecutedQuantity: newO.AccBaseVolume, + IsWorking: newO.Status.IsWorking(), + CreationTime: types.Time(newO.CTime), + UpdateTime: types.Time(newO.UTime), + }, res) + }) + + // limit buy example: + // { + // "instId":"BGBUSDT", + // "orderId":"1107955329902481408", + // "clientOid":"c578164a-bf34-44ba-8bb7-a1538f33b1b8", + // "price":"0.49998", + // "size":"24.9990", + // "notional":"24.999000", + // "orderType":"limit", + // "force":"gtc", + // "side":"buy", + // "fillPrice":"0.49998", + // "tradeId":"1107955401758285828", + // "baseVolume":"15.9404", + // "fillTime":"1699883073272", + // "fillFee":"-0.0159404", + // "fillFeeCoin":"BGB", + // "tradeScope":"M", + // "accBaseVolume":"15.9404", + // "priceAvg":"0.49998", + // "status":"partially_filled", + // "cTime":"1699883056140", + // "uTime":"1699883073285", + // "feeDetail":[ + // { + // "feeCoin":"BGB", + // "fee":"-0.0159404" + // } + // ], + // "enterPointSource":"API" + // } + t.Run("limit buy", func(t *testing.T) { + newO := o + newO.OrderType = v2.OrderTypeLimit + + res, err := newO.toGlobalOrder() + assert.NoError(t, err) + assert.Equal(t, types.Order{ + SubmitOrder: types.SubmitOrder{ + ClientOrderID: "cc73aab9-1e44-4022-8458-60d8c6a08753", + Symbol: "BGBUSDT", + Side: types.SideTypeBuy, + Type: types.OrderTypeLimit, + Quantity: newO.Size, + Price: newO.PriceAvg, + TimeInForce: types.TimeInForceGTC, + }, + Exchange: types.ExchangeBitget, + OrderID: uint64(newO.OrderId), + UUID: strconv.FormatInt(int64(newO.OrderId), 10), + Status: types.OrderStatusPartiallyFilled, + ExecutedQuantity: newO.AccBaseVolume, + IsWorking: newO.Status.IsWorking(), + CreationTime: types.Time(newO.CTime), + UpdateTime: types.Time(newO.UTime), + }, res) + }) + + // limit sell example: + // { + // "instId":"BGBUSDT", + // "orderId":"1107936497259417600", + // "clientOid":"02d4592e-091c-4b5a-aef3-6a7cf57b5e82", + // "price":"0.48710", + // "size":"280.0000", + // "orderType":"limit", + // "force":"gtc", + // "side":"sell", + // "fillPrice":"0.48710", + // "tradeId":"1107937053540556809", + // "baseVolume":"41.0593", + // "fillTime":"1699878698716", + // "fillFee":"-0.01999998503", + // "fillFeeCoin":"USDT", + // "tradeScope":"M", + // "accBaseVolume":"146.3209", + // "priceAvg":"0.48710", + // "status":"partially_filled", + // "cTime":"1699878566088", + // "uTime":"1699878698746", + // "feeDetail":[ + // { + // "feeCoin":"USDT", + // "fee":"-0.07127291039" + // } + // ], + // "enterPointSource":"API" + // } + t.Run("limit sell", func(t *testing.T) { + newO := o + newO.OrderType = v2.OrderTypeLimit + newO.Side = v2.SideTypeSell + + res, err := newO.toGlobalOrder() + assert.NoError(t, err) + assert.Equal(t, types.Order{ + SubmitOrder: types.SubmitOrder{ + ClientOrderID: "cc73aab9-1e44-4022-8458-60d8c6a08753", + Symbol: "BGBUSDT", + Side: types.SideTypeSell, + Type: types.OrderTypeLimit, + Quantity: newO.Size, + Price: newO.PriceAvg, + TimeInForce: types.TimeInForceGTC, + }, + Exchange: types.ExchangeBitget, + OrderID: uint64(newO.OrderId), + UUID: strconv.FormatInt(int64(newO.OrderId), 10), + Status: types.OrderStatusPartiallyFilled, + ExecutedQuantity: newO.AccBaseVolume, + IsWorking: newO.Status.IsWorking(), + CreationTime: types.Time(newO.CTime), + UpdateTime: types.Time(newO.UTime), + }, res) + }) + + t.Run("unexpected status", func(t *testing.T) { + newO := o + newO.Status = "xxx" + _, err := newO.toGlobalOrder() + assert.ErrorContains(t, err, "xxx") + }) + + t.Run("unexpected time-in-force", func(t *testing.T) { + newO := o + newO.Force = "xxx" + _, err := newO.toGlobalOrder() + assert.ErrorContains(t, err, "xxx") + }) + + t.Run("unexpected order type", func(t *testing.T) { + newO := o + newO.OrderType = "xxx" + _, err := newO.toGlobalOrder() + assert.ErrorContains(t, err, "xxx") + }) + + t.Run("unexpected side", func(t *testing.T) { + newO := o + newO.Side = "xxx" + _, err := newO.toGlobalOrder() + assert.ErrorContains(t, err, "xxx") + }) +} + +func TestOrder_toGlobalTrade(t *testing.T) { + // market buy example: + // { + // "instId":"BGBUSDT", + // "orderId":"1107950489998626816", + // "clientOid":"cc73aab9-1e44-4022-8458-60d8c6a08753", + // "size":"39.0000", + // "notional":"39.000000", + // "orderType":"market", + // "force":"gtc", + // "side":"buy", + // "fillPrice":"0.49016", + // "tradeId":"1107950490073112582", + // "baseVolume":"33.6558", + // "fillTime":"1699881902235", + // "fillFee":"-0.0336558", + // "fillFeeCoin":"BGB", + // "tradeScope":"T", + // "accBaseVolume":"33.6558", + // "priceAvg":"0.49016", + // "status":"partially_filled", + // "cTime":"1699881902217", + // "uTime":"1699881902248", + // "feeDetail":[ + // { + // "feeCoin":"BGB", + // "fee":"-0.0336558" + // } + // ], + // "enterPointSource":"API" + // } + o := Order{ + Trade: Trade{ + FillPrice: fixedpoint.NewFromFloat(0.49016), + TradeId: types.StrInt64(1107950490073112582), + BaseVolume: fixedpoint.NewFromFloat(33.6558), + FillTime: types.NewMillisecondTimestampFromInt(1699881902235), + FillFee: fixedpoint.NewFromFloat(-0.0336558), + FillFeeCoin: "BGB", + TradeScope: "T", + }, + InstId: "BGBUSDT", + OrderId: types.StrInt64(1107950489998626816), + ClientOrderId: "cc73aab9-1e44-4022-8458-60d8c6a08753", + Size: fixedpoint.NewFromFloat(39.0), + Notional: fixedpoint.NewFromFloat(39.0), + OrderType: v2.OrderTypeMarket, + Force: v2.OrderForceGTC, + Side: v2.SideTypeBuy, + AccBaseVolume: fixedpoint.NewFromFloat(33.6558), + PriceAvg: fixedpoint.NewFromFloat(0.49016), + Status: v2.OrderStatusPartialFilled, + CTime: types.NewMillisecondTimestampFromInt(1699881902217), + UTime: types.NewMillisecondTimestampFromInt(1699881902248), + FeeDetail: nil, + EnterPointSource: "API", + } + + t.Run("succeeds", func(t *testing.T) { + res, err := o.toGlobalTrade() + assert.NoError(t, err) + assert.Equal(t, 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: "BGBUSDT", + Side: types.SideTypeBuy, + IsBuyer: true, + IsMaker: false, + Time: types.Time(o.FillTime), + Fee: o.FillFee.Abs(), + FeeCurrency: "BGB", + }, res) + }) + + t.Run("unexpected trade scope", func(t *testing.T) { + newO := o + newO.TradeScope = "xxx" + _, err := newO.toGlobalTrade() + assert.ErrorContains(t, err, "xxx") + }) + + t.Run("unexpected side type", func(t *testing.T) { + newO := o + newO.Side = "xxx" + _, err := newO.toGlobalTrade() + assert.ErrorContains(t, err, "xxx") + }) + + t.Run("unexpected side type", func(t *testing.T) { + newO := o + newO.Status = "xxx" + _, err := newO.toGlobalTrade() + assert.ErrorContains(t, err, "xxx") + }) +} diff --git a/pkg/exchange/bitget/stream.go b/pkg/exchange/bitget/stream.go index 00ecae426..bdab89774 100644 --- a/pkg/exchange/bitget/stream.go +++ b/pkg/exchange/bitget/stream.go @@ -25,12 +25,15 @@ var ( type Stream struct { types.StandardStream + privateChannelSymbols []string + key, secret, passphrase string bookEventCallbacks []func(o BookEvent) marketTradeEventCallbacks []func(o MarketTradeEvent) KLineEventCallbacks []func(o KLineEvent) - accountEventCallbacks []func(e AccountEvent) + accountEventCallbacks []func(e AccountEvent) + orderTradeEventCallbacks []func(e OrderTradeEvent) lastCandle map[string]types.KLine } @@ -56,6 +59,7 @@ func NewStream(key, secret, passphrase string) *Stream { stream.OnAuth(stream.handleAuth) stream.OnAccountEvent(stream.handleAccountEvent) + stream.OnOrderTradeEvent(stream.handleOrderTradeEvent) return stream } @@ -129,25 +133,52 @@ func (s *Stream) dispatchEvent(event interface{}) { case *AccountEvent: s.EmitAccountEvent(*e) + case *OrderTradeEvent: + s.EmitOrderTradeEvent(*e) + + case []byte: + // We only handle the 'pong' case. Others are unexpected. + if !bytes.Equal(e, pongBytes) { + log.Errorf("invalid event: %q", e) + } } } +// handleAuth subscribe private stream channels. Because Bitget doesn't allow authentication and subscription to be used +// consecutively, we subscribe after authentication confirmation. func (s *Stream) handleAuth() { - if err := s.Conn.WriteJSON(WsOp{ + op := WsOp{ Op: WsEventSubscribe, Args: []WsArg{ { InstType: instSpV2, Channel: ChannelAccount, - Coin: "default", // default all + Coin: "default", // all coins }, }, - }); err != nil { + } + if len(s.privateChannelSymbols) > 0 { + for _, symbol := range s.privateChannelSymbols { + op.Args = append(op.Args, WsArg{ + InstType: instSpV2, + Channel: ChannelOrders, + InstId: symbol, + }) + } + } else { + log.Warnf("you have not subscribed to any order channels") + } + + if err := s.Conn.WriteJSON(op); err != nil { log.WithError(err).Error("failed to send subscription request") return } } +func (s *Stream) SetPrivateChannelSymbols(symbols []string) { + s.privateChannelSymbols = symbols +} + func (s *Stream) handlerConnect() { if s.PublicOnly { // errors are handled in the syncSubscriptions, so they are skipped here. @@ -279,6 +310,17 @@ func parseEvent(in []byte) (interface{}, error) { book.instId = event.Arg.InstId return &book, nil + case ChannelOrders: + var order OrderTradeEvent + err = json.Unmarshal(event.Data, &order.Orders) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal data into OrderTradeEvent, Arg: %+v Data: %s, err: %w", event.Arg, string(event.Data), err) + } + + order.actionType = event.Action + order.instId = event.Arg.InstId + return &order, nil + case ChannelTrade: var trade MarketTradeEvent err = json.Unmarshal(event.Data, &trade.Events) @@ -364,3 +406,31 @@ func (s *Stream) handleAccountEvent(m AccountEvent) { } s.StandardStream.EmitBalanceSnapshot(balanceMap) } + +func (s *Stream) handleOrderTradeEvent(m OrderTradeEvent) { + if len(m.Orders) == 0 { + return + } + + for _, order := range m.Orders { + globalOrder, err := order.toGlobalOrder() + if err != nil { + log.Errorf("failed to convert order to global: %s", err) + continue + } + // The bitget support only snapshot on orders channel, so we use snapshot as update to emit data. + if m.actionType != ActionTypeSnapshot { + continue + } + s.StandardStream.EmitOrderUpdate(globalOrder) + + if globalOrder.Status == types.OrderStatusPartiallyFilled { + trade, err := order.toGlobalTrade() + if err != nil { + log.Errorf("failed to convert trade to global: %s", err) + continue + } + s.StandardStream.EmitTradeUpdate(trade) + } + } +} diff --git a/pkg/exchange/bitget/stream_callbacks.go b/pkg/exchange/bitget/stream_callbacks.go index 44661171e..01dea0f5f 100644 --- a/pkg/exchange/bitget/stream_callbacks.go +++ b/pkg/exchange/bitget/stream_callbacks.go @@ -43,3 +43,13 @@ func (s *Stream) EmitAccountEvent(e AccountEvent) { cb(e) } } + +func (s *Stream) OnOrderTradeEvent(cb func(e OrderTradeEvent)) { + s.orderTradeEventCallbacks = append(s.orderTradeEventCallbacks, cb) +} + +func (s *Stream) EmitOrderTradeEvent(e OrderTradeEvent) { + for _, cb := range s.orderTradeEventCallbacks { + cb(e) + } +} diff --git a/pkg/exchange/bitget/stream_test.go b/pkg/exchange/bitget/stream_test.go index 273941e7d..011210abe 100644 --- a/pkg/exchange/bitget/stream_test.go +++ b/pkg/exchange/bitget/stream_test.go @@ -134,6 +134,12 @@ func TestStream(t *testing.T) { s.OnBalanceUpdate(func(balances types.BalanceMap) { t.Log("get update", balances) }) + s.OnOrderUpdate(func(order types.Order) { + t.Log("order update", order) + }) + s.OnTradeUpdate(func(trade types.Trade) { + t.Log("trade update", trade) + }) c := make(chan struct{}) <-c diff --git a/pkg/exchange/bitget/types.go b/pkg/exchange/bitget/types.go index 19c87c484..06425c64f 100644 --- a/pkg/exchange/bitget/types.go +++ b/pkg/exchange/bitget/types.go @@ -6,6 +6,7 @@ import ( "fmt" "time" + v2 "github.com/c9s/bbgo/pkg/exchange/bitget/bitgetapi/v2" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" ) @@ -29,6 +30,7 @@ const ( // ChannelOrderBook15 top 15 order book of "books" that begins from bid1/ask1 ChannelOrderBook15 ChannelType = "books15" ChannelTrade ChannelType = "trade" + ChannelOrders ChannelType = "orders" ) type WsArg struct { @@ -460,3 +462,63 @@ type AccountEvent struct { actionType ActionType instId string } + +type Trade struct { + // Latest filled price + FillPrice fixedpoint.Value `json:"fillPrice"` + TradeId types.StrInt64 `json:"tradeId"` + // Number of latest filled orders + BaseVolume fixedpoint.Value `json:"baseVolume"` + FillTime types.MillisecondTimestamp `json:"fillTime"` + // Transaction fee of the latest transaction, negative value + FillFee fixedpoint.Value `json:"fillFee"` + // Currency of transaction fee of the latest transaction + FillFeeCoin string `json:"fillFeeCoin"` + // Direction of liquidity of the latest transaction + TradeScope string `json:"tradeScope"` +} + +type Order struct { + Trade + + InstId string `json:"instId"` + // OrderId are always numeric. It's confirmed with official customer service. https://t.me/bitgetOpenapi/24172 + OrderId types.StrInt64 `json:"orderId"` + ClientOrderId string `json:"clientOid"` + // Size is base coin when orderType=limit; quote coin when orderType=market + Size fixedpoint.Value `json:"size"` + // Buy amount, returned when buying at market price + Notional fixedpoint.Value `json:"notional"` + OrderType v2.OrderType `json:"orderType"` + Force v2.OrderForce `json:"force"` + Side v2.SideType `json:"side"` + AccBaseVolume fixedpoint.Value `json:"accBaseVolume"` + PriceAvg fixedpoint.Value `json:"priceAvg"` + Status v2.OrderStatus `json:"status"` + CTime types.MillisecondTimestamp `json:"cTime"` + UTime types.MillisecondTimestamp `json:"uTime"` + FeeDetail []struct { + FeeCoin string `json:"feeCoin"` + Fee string `json:"fee"` + } `json:"feeDetail"` + EnterPointSource string `json:"enterPointSource"` +} + +func (o *Order) isMaker() (bool, error) { + switch o.TradeScope { + case "T": + return false, nil + case "M": + return true, nil + default: + return false, fmt.Errorf("unexpected trade scope: %s", o.TradeScope) + } +} + +type OrderTradeEvent struct { + Orders []Order + + // internal use + actionType ActionType + instId string +}