mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-21 22:43:52 +00:00
pkg/exchange: implement order trade user stream
This commit is contained in:
parent
720fe2e12e
commit
4f94f7acc0
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user