mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-22 14:55:16 +00:00
Merge pull request #1412 from c9s/edwin/bitget/add-order-event
FEATURE: [bitget] implement order, trade user stream
This commit is contained in:
commit
579e8b0ae5
|
@ -58,6 +58,10 @@ type ExchangeSession struct {
|
|||
// This option is exchange specific
|
||||
PrivateChannels []string `json:"privateChannels,omitempty" yaml:"privateChannels,omitempty"`
|
||||
|
||||
// PrivateChannelSymbols is used for filtering the private user data channel, .e.g, order symbol subscription.
|
||||
// This option is exchange specific
|
||||
PrivateChannelSymbols []string `json:"privateChannelSymbols,omitempty" yaml:"privateChannelSymbols,omitempty"`
|
||||
|
||||
Margin bool `json:"margin,omitempty" yaml:"margin"`
|
||||
IsolatedMargin bool `json:"isolatedMargin,omitempty" yaml:"isolatedMargin,omitempty"`
|
||||
IsolatedMarginSymbol string `json:"isolatedMarginSymbol,omitempty" yaml:"isolatedMarginSymbol,omitempty"`
|
||||
|
@ -248,6 +252,11 @@ func (session *ExchangeSession) Init(ctx context.Context, environ *Environment)
|
|||
setter.SetPrivateChannels(session.PrivateChannels)
|
||||
}
|
||||
}
|
||||
if len(session.PrivateChannelSymbols) > 0 {
|
||||
if setter, ok := session.UserDataStream.(types.PrivateChannelSymbolSetter); ok {
|
||||
setter.SetPrivateChannelSymbols(session.PrivateChannelSymbols)
|
||||
}
|
||||
}
|
||||
|
||||
logger.Infof("querying account balances...")
|
||||
account, err := session.Exchange.QueryAccount(ctx)
|
||||
|
|
|
@ -51,8 +51,8 @@ type OrderDetail struct {
|
|||
// The value is json string, so we unmarshal it after unmarshal OrderDetail
|
||||
FeeDetailRaw string `json:"feeDetail"`
|
||||
OrderSource string `json:"orderSource"`
|
||||
CTime types.MillisecondTimestamp `json:"cTime"`
|
||||
UTime types.MillisecondTimestamp `json:"uTime"`
|
||||
CreatedTime types.MillisecondTimestamp `json:"cTime"`
|
||||
UpdatedTime types.MillisecondTimestamp `json:"uTime"`
|
||||
|
||||
FeeDetail FeeDetail
|
||||
}
|
||||
|
|
|
@ -53,8 +53,8 @@ func TestOrderDetail_UnmarshalJSON(t *testing.T) {
|
|||
EnterPointSource: "API",
|
||||
FeeDetailRaw: "",
|
||||
OrderSource: "normal",
|
||||
CTime: types.NewMillisecondTimestampFromInt(1699021576683),
|
||||
UTime: types.NewMillisecondTimestampFromInt(1699021649099),
|
||||
CreatedTime: types.NewMillisecondTimestampFromInt(1699021576683),
|
||||
UpdatedTime: types.NewMillisecondTimestampFromInt(1699021649099),
|
||||
FeeDetail: FeeDetail{},
|
||||
}, od)
|
||||
})
|
||||
|
@ -98,8 +98,8 @@ func TestOrderDetail_UnmarshalJSON(t *testing.T) {
|
|||
EnterPointSource: "API",
|
||||
FeeDetailRaw: `{"newFees":{"c":0,"d":0,"deduction":false,"r":-0.0070005,"t":-0.0070005,"totalDeductionFee":0},"USDT":{"deduction":false,"feeCoinCode":"USDT","totalDeductionFee":0,"totalFee":-0.007000500000}}`,
|
||||
OrderSource: "normal",
|
||||
CTime: types.NewMillisecondTimestampFromInt(1699020564659),
|
||||
UTime: types.NewMillisecondTimestampFromInt(1699020564688),
|
||||
CreatedTime: types.NewMillisecondTimestampFromInt(1699020564659),
|
||||
UpdatedTime: types.NewMillisecondTimestampFromInt(1699020564688),
|
||||
FeeDetail: FeeDetail{
|
||||
NewFees: struct {
|
||||
DeductedByCoupon fixedpoint.Value `json:"c"`
|
||||
|
|
|
@ -47,8 +47,8 @@ type Trade struct {
|
|||
Amount fixedpoint.Value `json:"amount"`
|
||||
FeeDetail TradeFee `json:"feeDetail"`
|
||||
TradeScope TradeScope `json:"tradeScope"`
|
||||
CTime types.MillisecondTimestamp `json:"cTime"`
|
||||
UTime types.MillisecondTimestamp `json:"uTime"`
|
||||
CreatedTime types.MillisecondTimestamp `json:"cTime"`
|
||||
UpdatedTime types.MillisecondTimestamp `json:"uTime"`
|
||||
}
|
||||
|
||||
//go:generate GetRequest -url "/api/v2/spot/trade/fills" -type GetTradeFillsRequest -responseDataType []Trade
|
||||
|
|
|
@ -27,8 +27,8 @@ type UnfilledOrder struct {
|
|||
QuoteVolume fixedpoint.Value `json:"quoteVolume"`
|
||||
EnterPointSource string `json:"enterPointSource"`
|
||||
OrderSource string `json:"orderSource"`
|
||||
CTime types.MillisecondTimestamp `json:"cTime"`
|
||||
UTime types.MillisecondTimestamp `json:"uTime"`
|
||||
CreatedTime types.MillisecondTimestamp `json:"cTime"`
|
||||
UpdatedTime types.MillisecondTimestamp `json:"uTime"`
|
||||
}
|
||||
|
||||
//go:generate GetRequest -url "/api/v2/spot/trade/unfilled-orders" -type GetUnfilledOrdersRequest -responseDataType []UnfilledOrder
|
||||
|
|
|
@ -158,7 +158,7 @@ func toGlobalTrade(trade v2.Trade) (*types.Trade, error) {
|
|||
Side: side,
|
||||
IsBuyer: side == types.SideTypeBuy,
|
||||
IsMaker: isMaker,
|
||||
Time: types.Time(trade.CTime),
|
||||
Time: types.Time(trade.CreatedTime),
|
||||
Fee: trade.FeeDetail.TotalFee.Abs(),
|
||||
FeeCurrency: trade.FeeDetail.FeeCoin,
|
||||
FeeDiscounted: isDiscount,
|
||||
|
@ -211,8 +211,8 @@ func unfilledOrderToGlobalOrder(order v2.UnfilledOrder) (*types.Order, error) {
|
|||
Status: status,
|
||||
ExecutedQuantity: order.BaseVolume,
|
||||
IsWorking: order.Status.IsWorking(),
|
||||
CreationTime: types.Time(order.CTime.Time()),
|
||||
UpdateTime: types.Time(order.UTime.Time()),
|
||||
CreationTime: types.Time(order.CreatedTime.Time()),
|
||||
UpdateTime: types.Time(order.UpdatedTime.Time()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -262,8 +262,8 @@ func toGlobalOrder(order v2.OrderDetail) (*types.Order, error) {
|
|||
Status: status,
|
||||
ExecutedQuantity: order.BaseVolume,
|
||||
IsWorking: order.Status.IsWorking(),
|
||||
CreationTime: types.Time(order.CTime.Time()),
|
||||
UpdateTime: types.Time(order.UTime.Time()),
|
||||
CreationTime: types.Time(order.CreatedTime.Time()),
|
||||
UpdateTime: types.Time(order.UpdatedTime.Time()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -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.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
|
||||
}
|
||||
|
|
|
@ -222,8 +222,8 @@ func Test_unfilledOrderToGlobalOrder(t *testing.T) {
|
|||
QuoteVolume: fixedpoint.NewFromFloat(0),
|
||||
EnterPointSource: "API",
|
||||
OrderSource: "normal",
|
||||
CTime: types.NewMillisecondTimestampFromInt(1660704288118),
|
||||
UTime: types.NewMillisecondTimestampFromInt(1660704288118),
|
||||
CreatedTime: types.NewMillisecondTimestampFromInt(1660704288118),
|
||||
UpdatedTime: types.NewMillisecondTimestampFromInt(1660704288118),
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -296,8 +296,8 @@ func Test_toGlobalOrder(t *testing.T) {
|
|||
EnterPointSource: "API",
|
||||
FeeDetailRaw: `{\"newFees\":{\"c\":0,\"d\":0,\"deduction\":false,\"r\":-0.0070005,\"t\":-0.0070005,\"totalDeductionFee\":0},\"USDT\":{\"deduction\":false,\"feeCoinCode\":\"USDT\",\"totalDeductionFee\":0,\"totalFee\":-0.007000500000}}`,
|
||||
OrderSource: "normal",
|
||||
CTime: types.NewMillisecondTimestampFromInt(1660704288118),
|
||||
UTime: types.NewMillisecondTimestampFromInt(1660704288118),
|
||||
CreatedTime: types.NewMillisecondTimestampFromInt(1660704288118),
|
||||
UpdatedTime: types.NewMillisecondTimestampFromInt(1660704288118),
|
||||
}
|
||||
|
||||
expOrder = &types.Order{
|
||||
|
@ -559,8 +559,8 @@ func Test_toGlobalTrade(t *testing.T) {
|
|||
TotalFee: fixedpoint.NewFromFloat(-0.0070005),
|
||||
},
|
||||
TradeScope: v2.TradeTaker,
|
||||
CTime: types.NewMillisecondTimestampFromInt(1699020564676),
|
||||
UTime: types.NewMillisecondTimestampFromInt(1699020564687),
|
||||
CreatedTime: types.NewMillisecondTimestampFromInt(1699020564676),
|
||||
UpdatedTime: types.NewMillisecondTimestampFromInt(1699020564687),
|
||||
}
|
||||
|
||||
res, err := toGlobalTrade(trade)
|
||||
|
@ -597,7 +597,7 @@ func Test_toGlobalBalanceMap(t *testing.T) {
|
|||
Frozen: fixedpoint.NewFromFloat(0.6),
|
||||
Locked: fixedpoint.NewFromFloat(0.7),
|
||||
LimitAvailable: fixedpoint.Zero,
|
||||
UTime: types.NewMillisecondTimestampFromInt(1699020564676),
|
||||
UpdatedTime: types.NewMillisecondTimestampFromInt(1699020564676),
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
@ -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,
|
||||
CreatedTime: types.NewMillisecondTimestampFromInt(1699881902217),
|
||||
UpdatedTime: 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.CreatedTime),
|
||||
UpdateTime: types.Time(newO.UpdatedTime),
|
||||
}, 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.CreatedTime),
|
||||
UpdateTime: types.Time(newO.UpdatedTime),
|
||||
}, 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.CreatedTime),
|
||||
UpdateTime: types.Time(newO.UpdatedTime),
|
||||
}, 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.CreatedTime),
|
||||
UpdateTime: types.Time(newO.UpdatedTime),
|
||||
}, 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,
|
||||
CreatedTime: types.NewMillisecondTimestampFromInt(1699881902217),
|
||||
UpdatedTime: 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")
|
||||
})
|
||||
}
|
||||
|
|
|
@ -394,7 +394,7 @@ func (e *Exchange) QueryOpenOrders(ctx context.Context, symbol string) (orders [
|
|||
return orders, nil
|
||||
}
|
||||
|
||||
// QueryClosedOrders queries closed order by time range(`CTime`) and id. The order of the response is in descending order.
|
||||
// QueryClosedOrders queries closed order by time range(`CreatedTime`) and id. The order of the response is in descending order.
|
||||
// If you need to retrieve all data, please utilize the function pkg/exchange/batch.ClosedOrderBatchQuery.
|
||||
//
|
||||
// ** Since is inclusive, Until is exclusive. If you use a time range to query, you must provide both a start time and an end time. **
|
||||
|
@ -495,7 +495,7 @@ func (e *Exchange) CancelOrders(ctx context.Context, orders ...types.Order) (err
|
|||
}
|
||||
|
||||
// QueryTrades queries fill trades. The trade of the response is in descending order. The time-based query are typically
|
||||
// using (`CTime`) as the search criteria.
|
||||
// using (`CreatedTime`) as the search criteria.
|
||||
// If you need to retrieve all data, please utilize the function pkg/exchange/batch.TradeBatchQuery.
|
||||
//
|
||||
// ** StartTime is inclusive, EndTime is exclusive. If you use the EndTime, the StartTime is required. **
|
||||
|
|
|
@ -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)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
@ -450,7 +452,7 @@ type Balance struct {
|
|||
Locked fixedpoint.Value `json:"locked"`
|
||||
// Restricted availability For spot copy trading
|
||||
LimitAvailable fixedpoint.Value `json:"limitAvailable"`
|
||||
UTime types.MillisecondTimestamp `json:"uTime"`
|
||||
UpdatedTime types.MillisecondTimestamp `json:"uTime"`
|
||||
}
|
||||
|
||||
type AccountEvent 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"`
|
||||
CreatedTime types.MillisecondTimestamp `json:"cTime"`
|
||||
UpdatedTime 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
|
||||
}
|
||||
|
|
|
@ -46,6 +46,10 @@ type PrivateChannelSetter interface {
|
|||
SetPrivateChannels(channels []string)
|
||||
}
|
||||
|
||||
type PrivateChannelSymbolSetter interface {
|
||||
SetPrivateChannelSymbols(symbols []string)
|
||||
}
|
||||
|
||||
type Unsubscriber interface {
|
||||
// Unsubscribe unsubscribes the all subscriptions.
|
||||
Unsubscribe()
|
||||
|
|
Loading…
Reference in New Issue
Block a user