bbgo_origin/pkg/exchange/bybit/stream_test.go

565 lines
14 KiB
Go

package bybit
import (
"context"
"errors"
"fmt"
"os"
"strconv"
"testing"
"time"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
"github.com/c9s/bbgo/pkg/exchange/bybit/bybitapi"
"github.com/c9s/bbgo/pkg/exchange/bybit/mocks"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/testutil"
"github.com/c9s/bbgo/pkg/types"
)
func getTestClientOrSkip(t *testing.T) *Stream {
if b, _ := strconv.ParseBool(os.Getenv("CI")); b {
t.Skip("skip test for CI")
}
key, secret, ok := testutil.IntegrationTestConfigured(t, "BYBIT")
if !ok {
t.Skip("BYBIT_* env vars are not configured")
return nil
}
exchange, err := New(key, secret)
assert.NoError(t, err)
return NewStream(key, secret, exchange)
}
func TestStream(t *testing.T) {
t.Skip()
s := getTestClientOrSkip(t)
symbols := []string{
"BTCUSDT",
"ETHUSDT",
"DOTUSDT",
"ADAUSDT",
"AAVEUSDT",
"APTUSDT",
"ATOMUSDT",
"AXSUSDT",
"BNBUSDT",
"SOLUSDT",
"DOGEUSDT",
}
t.Run("Auth test", func(t *testing.T) {
s.OnBalanceSnapshot(func(balances types.BalanceMap) {
t.Log("got balance snapshot", balances)
})
s.Connect(context.Background())
c := make(chan struct{})
<-c
})
t.Run("book test", func(t *testing.T) {
s.Subscribe(types.BookChannel, "BTCUSDT", types.SubscribeOptions{
Depth: types.DepthLevel50,
})
s.SetPublicOnly()
err := s.Connect(context.Background())
assert.NoError(t, err)
s.OnBookSnapshot(func(book types.SliceOrderBook) {
t.Log("got snapshot", book)
})
s.OnBookUpdate(func(book types.SliceOrderBook) {
t.Log("got update", book)
})
c := make(chan struct{})
<-c
})
t.Run("book test on unsubscribe and reconnect", func(t *testing.T) {
for _, symbol := range symbols {
s.Subscribe(types.BookChannel, symbol, types.SubscribeOptions{
Depth: types.DepthLevel50,
})
}
s.SetPublicOnly()
err := s.Connect(context.Background())
assert.NoError(t, err)
s.OnBookSnapshot(func(book types.SliceOrderBook) {
t.Log("got snapshot", book)
})
s.OnBookUpdate(func(book types.SliceOrderBook) {
t.Log("got update", book)
})
<-time.After(2 * time.Second)
s.Unsubscribe()
for _, symbol := range symbols {
s.Subscribe(types.BookChannel, symbol, types.SubscribeOptions{
Depth: types.DepthLevel50,
})
}
<-time.After(2 * time.Second)
s.Reconnect()
c := make(chan struct{})
<-c
})
t.Run("market trade test", func(t *testing.T) {
s.Subscribe(types.MarketTradeChannel, "BTCUSDT", types.SubscribeOptions{})
s.SetPublicOnly()
err := s.Connect(context.Background())
assert.NoError(t, err)
s.OnMarketTrade(func(trade types.Trade) {
t.Log("got update", trade)
})
c := make(chan struct{})
<-c
})
t.Run("wallet test", func(t *testing.T) {
s.OnAuth(func() {
t.Log("authenticated")
})
err := s.Connect(context.Background())
assert.NoError(t, err)
s.OnBalanceUpdate(func(balances types.BalanceMap) {
t.Log("got update", balances)
})
c := make(chan struct{})
<-c
})
t.Run("order test", func(t *testing.T) {
err := s.Connect(context.Background())
assert.NoError(t, err)
s.OnOrderUpdate(func(order types.Order) {
t.Log("got update", order)
})
c := make(chan struct{})
<-c
})
t.Run("kline test", func(t *testing.T) {
s.Subscribe(types.KLineChannel, "BTCUSDT", types.SubscribeOptions{
Interval: types.Interval30m,
Depth: "",
Speed: "",
})
s.SetPublicOnly()
err := s.Connect(context.Background())
assert.NoError(t, err)
s.OnKLine(func(kline types.KLine) {
t.Log(kline)
})
c := make(chan struct{})
<-c
})
t.Run("trade test", func(t *testing.T) {
err := s.Connect(context.Background())
assert.NoError(t, err)
s.OnTradeUpdate(func(trade types.Trade) {
t.Log("got update", trade)
})
c := make(chan struct{})
<-c
})
}
func TestStream_parseWebSocketEvent(t *testing.T) {
s := Stream{}
t.Run("op", func(t *testing.T) {
input := `{
"success":true,
"ret_msg":"subscribe",
"conn_id":"a403c8e5-e2b6-4edd-a8f0-1a64fa7227a5",
"op":"subscribe"
}`
res, err := s.parseWebSocketEvent([]byte(input))
assert.NoError(t, err)
opEvent, ok := res.(*WebSocketOpEvent)
assert.True(t, ok)
expSucceeds := true
expRetMsg := "subscribe"
assert.Equal(t, WebSocketOpEvent{
Success: expSucceeds,
RetMsg: expRetMsg,
ReqId: "",
ConnId: "a403c8e5-e2b6-4edd-a8f0-1a64fa7227a5",
Op: WsOpTypeSubscribe,
Args: nil,
}, *opEvent)
})
t.Run("TopicTypeOrderBook with delta", func(t *testing.T) {
input := `{
"topic":"orderbook.50.BTCUSDT",
"ts":1691130685111,
"type":"delta",
"data":{
"s":"BTCUSDT",
"b":[
],
"a":[
[
"29239.37",
"0.082356"
],
[
"29236.1",
"0"
]
],
"u":1854104,
"seq":10559247733
}
}`
res, err := s.parseWebSocketEvent([]byte(input))
assert.NoError(t, err)
book, ok := res.(*BookEvent)
assert.True(t, ok)
assert.Equal(t, BookEvent{
Symbol: "BTCUSDT",
Bids: nil,
Asks: types.PriceVolumeSlice{
{
fixedpoint.NewFromFloat(29239.37),
fixedpoint.NewFromFloat(0.082356),
},
{
fixedpoint.NewFromFloat(29236.1),
fixedpoint.NewFromFloat(0),
},
},
UpdateId: fixedpoint.NewFromFloat(1854104),
SequenceId: fixedpoint.NewFromFloat(10559247733),
Type: DataTypeDelta,
ServerTime: types.NewMillisecondTimestampFromInt(1691130685111).Time(),
}, *book)
})
t.Run("TopicTypeMarketTrade with snapshot", func(t *testing.T) {
input := `{
"topic":"publicTrade.BTCUSDT",
"ts":1694348711526,
"type":"snapshot",
"data":[
{
"i":"2290000000068683805",
"T":1694348711524,
"p":"25816.27",
"v":"0.000083",
"S":"Sell",
"s":"BTCUSDT",
"BT":false
}
]
}`
res, err := s.parseWebSocketEvent([]byte(input))
assert.NoError(t, err)
book, ok := res.([]MarketTradeEvent)
assert.True(t, ok)
assert.Equal(t, []MarketTradeEvent{
{
Timestamp: types.NewMillisecondTimestampFromInt(1694348711524),
Symbol: "BTCUSDT",
Side: bybitapi.SideSell,
Quantity: fixedpoint.NewFromFloat(0.000083),
Price: fixedpoint.NewFromFloat(25816.27),
Direction: "",
TradeId: "2290000000068683805",
BlockTrade: false,
},
}, book)
})
t.Run("TopicTypeKLine with snapshot", func(t *testing.T) {
input := `{
"topic": "kline.5.BTCUSDT",
"data": [
{
"start": 1672324800000,
"end": 1672325099999,
"interval": "5",
"open": "16649.5",
"close": "16677",
"high": "16677",
"low": "16608",
"volume": "2.081",
"turnover": "34666.4005",
"confirm": false,
"timestamp": 1672324988882
}
],
"ts": 1672324988882,
"type": "snapshot"
}`
res, err := s.parseWebSocketEvent([]byte(input))
assert.NoError(t, err)
book, ok := res.(*KLineEvent)
assert.True(t, ok)
assert.Equal(t, KLineEvent{
Symbol: "BTCUSDT",
Type: DataTypeSnapshot,
KLines: []KLine{
{
StartTime: types.NewMillisecondTimestampFromInt(1672324800000),
EndTime: types.NewMillisecondTimestampFromInt(1672325099999),
Interval: "5",
OpenPrice: fixedpoint.NewFromFloat(16649.5),
ClosePrice: fixedpoint.NewFromFloat(16677),
HighPrice: fixedpoint.NewFromFloat(16677),
LowPrice: fixedpoint.NewFromFloat(16608),
Volume: fixedpoint.NewFromFloat(2.081),
Turnover: fixedpoint.NewFromFloat(34666.4005),
Confirm: false,
Timestamp: types.NewMillisecondTimestampFromInt(1672324988882),
},
},
}, *book)
})
t.Run("TopicTypeKLine with invalid topic", func(t *testing.T) {
input := `{
"topic": "kline.5",
"data": [
{
"start": 1672324800000,
"end": 1672325099999,
"interval": "5",
"open": "16649.5",
"close": "16677",
"high": "16677",
"low": "16608",
"volume": "2.081",
"turnover": "34666.4005",
"confirm": false,
"timestamp": 1672324988882
}
],
"ts": 1672324988882,
"type": "snapshot"
}`
res, err := s.parseWebSocketEvent([]byte(input))
assert.Equal(t, errors.New("unexpected topic: kline.5"), err)
assert.Nil(t, res)
})
t.Run("Parse fails", func(t *testing.T) {
input := `{
"topic":"orderbook.50.BTCUSDT",
"ts":1691130685111,
"type":"delta",
"data":{
"GG": "test",
}
}`
res, err := s.parseWebSocketEvent([]byte(input))
assert.Error(t, err)
assert.Equal(t, nil, res)
})
}
func Test_convertSubscription(t *testing.T) {
s := Stream{}
t.Run("BookChannel.DepthLevel1", func(t *testing.T) {
res, err := s.convertSubscription(types.Subscription{
Symbol: "BTCUSDT",
Channel: types.BookChannel,
Options: types.SubscribeOptions{
Depth: types.DepthLevel1,
},
})
assert.NoError(t, err)
assert.Equal(t, genTopic(TopicTypeOrderBook, types.DepthLevel1, "BTCUSDT"), res)
})
t.Run("BookChannel. with default depth", func(t *testing.T) {
res, err := s.convertSubscription(types.Subscription{
Symbol: "BTCUSDT",
Channel: types.BookChannel,
})
assert.NoError(t, err)
assert.Equal(t, genTopic(TopicTypeOrderBook, types.DepthLevel1, "BTCUSDT"), res)
})
t.Run("BookChannel.DepthLevel50", func(t *testing.T) {
res, err := s.convertSubscription(types.Subscription{
Symbol: "BTCUSDT",
Channel: types.BookChannel,
Options: types.SubscribeOptions{
Depth: types.DepthLevel50,
},
})
assert.NoError(t, err)
assert.Equal(t, genTopic(TopicTypeOrderBook, types.DepthLevel50, "BTCUSDT"), res)
})
t.Run("BookChannel. not support depth, use default level 1", func(t *testing.T) {
res, err := s.convertSubscription(types.Subscription{
Symbol: "BTCUSDT",
Channel: types.BookChannel,
Options: types.SubscribeOptions{
Depth: "20",
},
})
assert.NoError(t, err)
assert.Equal(t, genTopic(TopicTypeOrderBook, types.DepthLevel1, "BTCUSDT"), res)
})
t.Run("unsupported channel", func(t *testing.T) {
res, err := s.convertSubscription(types.Subscription{
Symbol: "BTCUSDT",
Channel: "unsupported",
})
assert.Equal(t, fmt.Errorf("unsupported stream channel: %s", "unsupported"), err)
assert.Equal(t, "", res)
})
t.Run("MarketTradeChannel", func(t *testing.T) {
res, err := s.convertSubscription(types.Subscription{
Symbol: "BTCUSDT",
Channel: types.MarketTradeChannel,
Options: types.SubscribeOptions{},
})
assert.NoError(t, err)
assert.Equal(t, genTopic(TopicTypeMarketTrade, "BTCUSDT"), res)
})
}
func TestStream_getFeeRate(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
unknownErr := errors.New("unknown err")
t.Run("succeeds", func(t *testing.T) {
mockMarketProvider := mocks.NewMockStreamDataProvider(mockCtrl)
s := &Stream{
streamDataProvider: mockMarketProvider,
}
ctx := context.Background()
feeRates := bybitapi.FeeRates{
List: []bybitapi.FeeRate{
{
Symbol: "BTCUSDT",
TakerFeeRate: fixedpoint.NewFromFloat(0.001),
MakerFeeRate: fixedpoint.NewFromFloat(0.001),
},
{
Symbol: "ETHUSDT",
TakerFeeRate: fixedpoint.NewFromFloat(0.001),
MakerFeeRate: fixedpoint.NewFromFloat(0.001),
},
{
Symbol: "OPTIONCOIN",
TakerFeeRate: fixedpoint.NewFromFloat(0.001),
MakerFeeRate: fixedpoint.NewFromFloat(0.001),
},
},
}
mkts := types.MarketMap{
"BTCUSDT": types.Market{
Symbol: "BTCUSDT",
QuoteCurrency: "USDT",
BaseCurrency: "BTC",
},
"ETHUSDT": types.Market{
Symbol: "ETHUSDT",
QuoteCurrency: "USDT",
BaseCurrency: "ETH",
},
}
mockMarketProvider.EXPECT().GetAllFeeRates(ctx).Return(feeRates, nil).Times(1)
mockMarketProvider.EXPECT().QueryMarkets(ctx).Return(mkts, nil).Times(1)
expFeeRates := map[string]*symbolFeeDetail{
"BTCUSDT": {
FeeRate: feeRates.List[0],
BaseCoin: "BTC",
QuoteCoin: "USDT",
},
"ETHUSDT": {
FeeRate: feeRates.List[1],
BaseCoin: "ETH",
QuoteCoin: "USDT",
},
}
err := s.getAllFeeRates(ctx)
assert.NoError(t, err)
assert.Equal(t, expFeeRates, s.symbolFeeDetails)
})
t.Run("failed to query markets", func(t *testing.T) {
mockMarketProvider := mocks.NewMockStreamDataProvider(mockCtrl)
s := &Stream{
streamDataProvider: mockMarketProvider,
}
ctx := context.Background()
feeRates := bybitapi.FeeRates{
List: []bybitapi.FeeRate{
{
Symbol: "BTCUSDT",
TakerFeeRate: fixedpoint.NewFromFloat(0.001),
MakerFeeRate: fixedpoint.NewFromFloat(0.001),
},
{
Symbol: "ETHUSDT",
TakerFeeRate: fixedpoint.NewFromFloat(0.001),
MakerFeeRate: fixedpoint.NewFromFloat(0.001),
},
{
Symbol: "OPTIONCOIN",
TakerFeeRate: fixedpoint.NewFromFloat(0.001),
MakerFeeRate: fixedpoint.NewFromFloat(0.001),
},
},
}
mockMarketProvider.EXPECT().GetAllFeeRates(ctx).Return(feeRates, nil).Times(1)
mockMarketProvider.EXPECT().QueryMarkets(ctx).Return(nil, unknownErr).Times(1)
err := s.getAllFeeRates(ctx)
assert.Equal(t, fmt.Errorf("failed to get markets: %w", unknownErr), err)
assert.Equal(t, map[string]*symbolFeeDetail(nil), s.symbolFeeDetails)
})
t.Run("failed to get fee rates", func(t *testing.T) {
mockMarketProvider := mocks.NewMockStreamDataProvider(mockCtrl)
s := &Stream{
streamDataProvider: mockMarketProvider,
}
ctx := context.Background()
mockMarketProvider.EXPECT().GetAllFeeRates(ctx).Return(bybitapi.FeeRates{}, unknownErr).Times(1)
err := s.getAllFeeRates(ctx)
assert.Equal(t, fmt.Errorf("failed to call get fee rates: %w", unknownErr), err)
assert.Equal(t, map[string]*symbolFeeDetail(nil), s.symbolFeeDetails)
})
}