package bybit import ( "context" "fmt" "math" "strconv" "testing" "time" "github.com/pkg/errors" "github.com/stretchr/testify/assert" "go.uber.org/multierr" "github.com/c9s/bbgo/pkg/exchange/bybit/bybitapi" v3 "github.com/c9s/bbgo/pkg/exchange/bybit/bybitapi/v3" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" ) func TestU(t *testing.T) { e := returnErr() t.Log(errors.Is(e, context.DeadlineExceeded)) } func returnErr() error { var err error return multierr.Append(multierr.Append(err, fmt.Errorf("got err: %w", context.DeadlineExceeded)), fmt.Errorf("GG")) } func TestToGlobalMarket(t *testing.T) { // sample: //{ // "Symbol": "BTCUSDT", // "BaseCoin": "BTC", // "QuoteCoin": "USDT", // "Innovation": 0, // "Status": "Trading", // "MarginTrading": "both", // "LotSizeFilter": { // "BasePrecision": 0.000001, // "QuotePrecision": 0.00000001, // "MinOrderQty": 0.000048, // "MaxOrderQty": 71.73956243, // "MinOrderAmt": 1, // "MaxOrderAmt": 2000000 // }, // "PriceFilter": { // "TickSize": 0.01 // } //} inst := bybitapi.Instrument{ Symbol: "BTCUSDT", BaseCoin: "BTC", QuoteCoin: "USDT", Innovation: "0", Status: bybitapi.StatusTrading, MarginTrading: "both", LotSizeFilter: struct { BasePrecision fixedpoint.Value `json:"basePrecision"` QuotePrecision fixedpoint.Value `json:"quotePrecision"` MinOrderQty fixedpoint.Value `json:"minOrderQty"` MaxOrderQty fixedpoint.Value `json:"maxOrderQty"` MinOrderAmt fixedpoint.Value `json:"minOrderAmt"` MaxOrderAmt fixedpoint.Value `json:"maxOrderAmt"` }{ BasePrecision: fixedpoint.NewFromFloat(0.000001), QuotePrecision: fixedpoint.NewFromFloat(0.00000001), MinOrderQty: fixedpoint.NewFromFloat(0.000048), MaxOrderQty: fixedpoint.NewFromFloat(71.73956243), MinOrderAmt: fixedpoint.NewFromInt(1), MaxOrderAmt: fixedpoint.NewFromInt(2000000), }, PriceFilter: struct { TickSize fixedpoint.Value `json:"tickSize"` }{ TickSize: fixedpoint.NewFromFloat(0.01), }, } exp := types.Market{ Symbol: inst.Symbol, LocalSymbol: inst.Symbol, PricePrecision: int(math.Log10(inst.LotSizeFilter.QuotePrecision.Float64())), VolumePrecision: int(math.Log10(inst.LotSizeFilter.BasePrecision.Float64())), QuoteCurrency: inst.QuoteCoin, BaseCurrency: inst.BaseCoin, MinNotional: inst.LotSizeFilter.MinOrderAmt, MinAmount: inst.LotSizeFilter.MinOrderAmt, MinQuantity: inst.LotSizeFilter.MinOrderQty, MaxQuantity: inst.LotSizeFilter.MaxOrderQty, StepSize: inst.LotSizeFilter.BasePrecision, MinPrice: inst.LotSizeFilter.MinOrderAmt, MaxPrice: inst.LotSizeFilter.MaxOrderAmt, TickSize: inst.PriceFilter.TickSize, } assert.Equal(t, toGlobalMarket(inst), exp) } func TestToGlobalTicker(t *testing.T) { // sample //{ // "symbol": "BTCUSDT", // "bid1Price": "28995.98", // "bid1Size": "4.741552", // "ask1Price": "28995.99", // "ask1Size": "0.16075", // "lastPrice": "28994", // "prevPrice24h": "29900", // "price24hPcnt": "-0.0303", // "highPrice24h": "30344.78", // "lowPrice24h": "28948.87", // "turnover24h": "184705500.13172874", // "volume24h": "6240.807096", // "usdIndexPrice": "28977.82001643" //} ticker := bybitapi.Ticker{ Symbol: "BTCUSDT", Bid1Price: fixedpoint.NewFromFloat(28995.98), Bid1Size: fixedpoint.NewFromFloat(4.741552), Ask1Price: fixedpoint.NewFromFloat(28995.99), Ask1Size: fixedpoint.NewFromFloat(0.16075), LastPrice: fixedpoint.NewFromFloat(28994), PrevPrice24H: fixedpoint.NewFromFloat(29900), Price24HPcnt: fixedpoint.NewFromFloat(-0.0303), HighPrice24H: fixedpoint.NewFromFloat(30344.78), LowPrice24H: fixedpoint.NewFromFloat(28948.87), Turnover24H: fixedpoint.NewFromFloat(184705500.13172874), Volume24H: fixedpoint.NewFromFloat(6240.807096), UsdIndexPrice: fixedpoint.NewFromFloat(28977.82001643), } timeNow := time.Now() exp := types.Ticker{ Time: timeNow, Volume: ticker.Volume24H, Last: ticker.LastPrice, Open: ticker.PrevPrice24H, High: ticker.HighPrice24H, Low: ticker.LowPrice24H, Buy: ticker.Bid1Price, Sell: ticker.Ask1Price, } assert.Equal(t, toGlobalTicker(ticker, timeNow), exp) } func TestToGlobalOrder(t *testing.T) { // sample: partialFilled //{ // "OrderId": 1472539279335923200, // "OrderLinkId": 1690276361150, // "BlockTradeId": null, // "Symbol": "DOTUSDT", // "Price": 7.278, // "Qty": 0.8, // "Side": "Sell", // "IsLeverage": 0, // "PositionIdx": 0, // "OrderStatus": "PartiallyFilled", // "CancelType": "UNKNOWN", // "RejectReason": null, // "AvgPrice": 7.278, // "LeavesQty": 0, // "LeavesValue": 0, // "CumExecQty": 0.5, // "CumExecValue": 0, // "CumExecFee": 0, // "TimeInForce": "GTC", // "OrderType": "Limit", // "StopOrderType": null, // "OrderIv": null, // "TriggerPrice": 0, // "TakeProfit": 0, // "StopLoss": 0, // "TpTriggerBy": null, // "SlTriggerBy": null, // "TriggerDirection": 0, // "TriggerBy": null, // "LastPriceOnCreated": null, // "ReduceOnly": false, // "CloseOnTrigger": false, // "SmpType": "None", // "SmpGroup": 0, // "SmpOrderId": null, // "TpslMode": null, // "TpLimitPrice": null, // "SlLimitPrice": null, // "PlaceType": null, // "CreatedTime": "2023-07-25 17:12:41.325 +0800 CST", // "UpdatedTime": "2023-07-25 17:12:57.868 +0800 CST" //} timeNow := time.Now() openOrder := bybitapi.Order{ OrderId: "1472539279335923200", OrderLinkId: "1690276361150", BlockTradeId: "", Symbol: "DOTUSDT", Price: fixedpoint.NewFromFloat(7.278), Qty: fixedpoint.NewFromFloat(0.8), Side: bybitapi.SideSell, IsLeverage: "0", PositionIdx: 0, OrderStatus: bybitapi.OrderStatusPartiallyFilled, CancelType: "UNKNOWN", RejectReason: "", AvgPrice: fixedpoint.NewFromFloat(7.728), LeavesQty: fixedpoint.NewFromFloat(0), LeavesValue: fixedpoint.NewFromFloat(0), CumExecQty: fixedpoint.NewFromFloat(0.5), CumExecValue: fixedpoint.NewFromFloat(0), CumExecFee: fixedpoint.NewFromFloat(0), TimeInForce: "GTC", OrderType: bybitapi.OrderTypeLimit, StopOrderType: "", OrderIv: "", TriggerPrice: fixedpoint.NewFromFloat(0), TakeProfit: fixedpoint.NewFromFloat(0), StopLoss: fixedpoint.NewFromFloat(0), TpTriggerBy: "", SlTriggerBy: "", TriggerDirection: 0, TriggerBy: "", LastPriceOnCreated: "", ReduceOnly: false, CloseOnTrigger: false, SmpType: "None", SmpGroup: 0, SmpOrderId: "", TpslMode: "", TpLimitPrice: "", SlLimitPrice: "", PlaceType: "", CreatedTime: types.MillisecondTimestamp(timeNow), UpdatedTime: types.MillisecondTimestamp(timeNow), } side, err := toGlobalSideType(openOrder.Side) assert.NoError(t, err) orderType, err := toGlobalOrderType(openOrder.OrderType) assert.NoError(t, err) tif, err := toGlobalTimeInForce(openOrder.TimeInForce) assert.NoError(t, err) status, err := toGlobalOrderStatus(openOrder.OrderStatus) assert.NoError(t, err) working, err := isWorking(openOrder.OrderStatus) assert.NoError(t, err) orderIdNum, err := strconv.ParseUint(openOrder.OrderId, 10, 64) assert.NoError(t, err) exp := types.Order{ SubmitOrder: types.SubmitOrder{ ClientOrderID: openOrder.OrderLinkId, Symbol: openOrder.Symbol, Side: side, Type: orderType, Quantity: openOrder.Qty, Price: openOrder.Price, TimeInForce: tif, }, Exchange: types.ExchangeBybit, OrderID: orderIdNum, UUID: openOrder.OrderId, Status: status, ExecutedQuantity: openOrder.CumExecQty, IsWorking: working, CreationTime: types.Time(timeNow), UpdateTime: types.Time(timeNow), IsFutures: false, IsMargin: false, IsIsolated: false, } res, err := toGlobalOrder(openOrder) assert.NoError(t, err) assert.Equal(t, res, &exp) } func TestToGlobalSideType(t *testing.T) { res, err := toGlobalSideType(bybitapi.SideBuy) assert.NoError(t, err) assert.Equal(t, types.SideTypeBuy, res) res, err = toGlobalSideType(bybitapi.SideSell) assert.NoError(t, err) assert.Equal(t, types.SideTypeSell, res) res, err = toGlobalSideType("GG") assert.Error(t, err) } func TestToGlobalOrderType(t *testing.T) { res, err := toGlobalOrderType(bybitapi.OrderTypeMarket) assert.NoError(t, err) assert.Equal(t, types.OrderTypeMarket, res) res, err = toGlobalOrderType(bybitapi.OrderTypeLimit) assert.NoError(t, err) assert.Equal(t, types.OrderTypeLimit, res) res, err = toGlobalOrderType("GG") assert.Error(t, err) } func TestToGlobalTimeInForce(t *testing.T) { res, err := toGlobalTimeInForce(bybitapi.TimeInForceGTC) assert.NoError(t, err) assert.Equal(t, types.TimeInForceGTC, res) res, err = toGlobalTimeInForce(bybitapi.TimeInForceIOC) assert.NoError(t, err) assert.Equal(t, types.TimeInForceIOC, res) res, err = toGlobalTimeInForce(bybitapi.TimeInForceFOK) assert.NoError(t, err) assert.Equal(t, types.TimeInForceFOK, res) res, err = toGlobalTimeInForce("GG") assert.Error(t, err) } func TestToGlobalOrderStatus(t *testing.T) { t.Run("New", func(t *testing.T) { res, err := toGlobalOrderStatus(bybitapi.OrderStatusNew) assert.NoError(t, err) assert.Equal(t, types.OrderStatusNew, res) res, err = toGlobalOrderStatus(bybitapi.OrderStatusActive) assert.NoError(t, err) assert.Equal(t, types.OrderStatusNew, res) }) t.Run("Filled", func(t *testing.T) { res, err := toGlobalOrderStatus(bybitapi.OrderStatusFilled) assert.NoError(t, err) assert.Equal(t, types.OrderStatusFilled, res) }) t.Run("PartiallyFilled", func(t *testing.T) { res, err := toGlobalOrderStatus(bybitapi.OrderStatusPartiallyFilled) assert.NoError(t, err) assert.Equal(t, types.OrderStatusPartiallyFilled, res) }) t.Run("OrderStatusCanceled", func(t *testing.T) { res, err := toGlobalOrderStatus(bybitapi.OrderStatusCancelled) assert.NoError(t, err) assert.Equal(t, types.OrderStatusCanceled, res) res, err = toGlobalOrderStatus(bybitapi.OrderStatusPartiallyFilledCanceled) assert.NoError(t, err) assert.Equal(t, types.OrderStatusCanceled, res) res, err = toGlobalOrderStatus(bybitapi.OrderStatusDeactivated) assert.NoError(t, err) assert.Equal(t, types.OrderStatusCanceled, res) }) t.Run("OrderStatusRejected", func(t *testing.T) { res, err := toGlobalOrderStatus(bybitapi.OrderStatusRejected) assert.NoError(t, err) assert.Equal(t, types.OrderStatusRejected, res) }) } func TestIsWorking(t *testing.T) { for _, s := range bybitapi.AllOrderStatuses { res, err := isWorking(s) assert.NoError(t, err) if res { gos, err := toGlobalOrderStatus(s) assert.NoError(t, err) assert.True(t, gos == types.OrderStatusNew || gos == types.OrderStatusPartiallyFilled) } } } func Test_toLocalOrderType(t *testing.T) { orderType, err := toLocalOrderType(types.OrderTypeLimit) assert.NoError(t, err) assert.Equal(t, bybitapi.OrderTypeLimit, orderType) orderType, err = toLocalOrderType(types.OrderTypeMarket) assert.NoError(t, err) assert.Equal(t, bybitapi.OrderTypeMarket, orderType) orderType, err = toLocalOrderType("wrong type") assert.Error(t, fmt.Errorf("order type %s not supported", "wrong side"), err) assert.Equal(t, bybitapi.OrderType(""), orderType) } func Test_toLocalSide(t *testing.T) { side, err := toLocalSide(types.SideTypeSell) assert.NoError(t, err) assert.Equal(t, bybitapi.SideSell, side) side, err = toLocalSide(types.SideTypeBuy) assert.NoError(t, err) assert.Equal(t, bybitapi.SideBuy, side) side, err = toLocalSide("wrong side") assert.Error(t, fmt.Errorf("side type %s not supported", "wrong side"), err) assert.Equal(t, bybitapi.Side(""), side) } func Test_toGlobalTrade(t *testing.T) { /* sample: trade { "Symbol":"BTCUSDT", "Id":"1474200510090276864", "OrderId":"1474200270671015936", "TradeId":"2100000000031181772", "OrderPrice":"27628", "OrderQty":"0.007959", "ExecFee":"0.21989125", "FeeTokenId":"USDT", "CreatTime":"2023-07-28 00:13:15.457 +0800 CST", "IsBuyer":"1", "IsMaker":"0", "MatchOrderId":"5760912963729109504", "MakerRebate":"0", "ExecutionTime":"2023-07-28 00:13:15.463 +0800 CST", "BlockTradeId": "", } */ timeNow := time.Now() trade := v3.Trade{ Symbol: "DOTUSDT", Id: "1474200510090276864", OrderId: "1474200270671015936", TradeId: "2100000000031181772", OrderPrice: fixedpoint.NewFromFloat(27628), OrderQty: fixedpoint.NewFromFloat(0.007959), ExecFee: fixedpoint.NewFromFloat(0.21989125), FeeTokenId: "USDT", CreatTime: types.MillisecondTimestamp(timeNow), IsBuyer: "0", IsMaker: "0", MatchOrderId: "5760912963729109504", MakerRebate: fixedpoint.NewFromFloat(0), ExecutionTime: types.MillisecondTimestamp(timeNow), BlockTradeId: "", } s, err := toV3Buyer(trade.IsBuyer) assert.NoError(t, err) m, err := toV3Maker(trade.IsMaker) assert.NoError(t, err) orderIdNum, err := strconv.ParseUint(trade.OrderId, 10, 64) assert.NoError(t, err) tradeId, err := strconv.ParseUint(trade.TradeId, 10, 64) assert.NoError(t, err) exp := types.Trade{ ID: tradeId, OrderID: orderIdNum, Exchange: types.ExchangeBybit, Price: trade.OrderPrice, Quantity: trade.OrderQty, QuoteQuantity: trade.OrderPrice.Mul(trade.OrderQty), Symbol: trade.Symbol, Side: s, IsBuyer: s == types.SideTypeBuy, IsMaker: m, Time: types.Time(timeNow), Fee: trade.ExecFee, FeeCurrency: trade.FeeTokenId, IsMargin: false, IsFutures: false, IsIsolated: false, } res, err := v3ToGlobalTrade(trade) assert.NoError(t, err) assert.Equal(t, res, &exp) }