mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-06-02 23:16:51 +00:00
exchanges/websocket: Implement subscription configuration (#1394)
* Websockets: Move Subscription to its own package This allows the small type to be imported from both `config` and from `stream` without an import cycle, so we don't have to repeat ourselves * Subs: Renamed Currency to Pair This was being mis-used through much of the code, and since we're already touching everything, we might as well fix it * Websockets: Add Subscription configuration * Binance: Add subscription configuration * Kucoin: Subscription configuration * Simplify GenerateDefaultSubs * Improve TestGenSubs coverage * Test Candle Sub generation * Support Candle intervals * Full responsibility for formatting Channel name on GenerateDefaultSubs OR consumer of Subscribe * Simplify generatePayloads as a result * Fix test coverage of asset types in processMarketSnapshot * Exchanges: Abstract ParallelChanOp * Tests: Generic ws mock instances * Kucoin: Fix intermittent conflict in test currs Use isolated test instance for `TestGetOpenInterest`. `TestGetOpenInterest` would occassionally change pairs before GenerateDefault Subs.
This commit is contained in:
@@ -19,6 +19,7 @@ import (
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/subscription"
|
||||
)
|
||||
|
||||
// Binance is the overarching type across the Binance package
|
||||
@@ -107,6 +108,13 @@ var (
|
||||
errEitherLoanOrCollateralAmountsMustBeSet = errors.New("either loan or collateral amounts must be set")
|
||||
)
|
||||
|
||||
var subscriptionNames = map[string]string{
|
||||
subscription.TickerChannel: "ticker",
|
||||
subscription.OrderbookChannel: "depth",
|
||||
subscription.CandlesChannel: "kline",
|
||||
subscription.AllTradesChannel: "trade",
|
||||
}
|
||||
|
||||
// GetExchangeInfo returns exchange information. Check binance_types for more
|
||||
// information
|
||||
func (b *Binance) GetExchangeInfo(ctx context.Context) (ExchangeInfo, error) {
|
||||
|
||||
@@ -24,6 +24,8 @@ import (
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/margin"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/sharedtestvalues"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/stream"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/subscription"
|
||||
"github.com/thrasher-corp/gocryptotrader/portfolio/withdraw"
|
||||
)
|
||||
|
||||
@@ -2007,7 +2009,7 @@ func TestWsTickerUpdate(t *testing.T) {
|
||||
func TestWsKlineUpdate(t *testing.T) {
|
||||
t.Parallel()
|
||||
pressXToJSON := []byte(`{"stream":"btcusdt@kline_1m","data":{
|
||||
"e": "kline",
|
||||
"e": "kline",
|
||||
"E": 123456789,
|
||||
"s": "BNBBTC",
|
||||
"k": {
|
||||
@@ -2404,15 +2406,63 @@ func TestSeedLocalCache(t *testing.T) {
|
||||
|
||||
func TestGenerateSubscriptions(t *testing.T) {
|
||||
t.Parallel()
|
||||
expected := []subscription.Subscription{}
|
||||
pairs, err := b.GetEnabledPairs(asset.Spot)
|
||||
assert.NoError(t, err, "GetEnabledPairs should not error")
|
||||
for _, p := range pairs {
|
||||
for _, c := range []string{"kline_1m", "depth@100ms", "ticker", "trade"} {
|
||||
expected = append(expected, subscription.Subscription{
|
||||
Channel: p.Format(currency.PairFormat{Delimiter: "", Uppercase: false}).String() + "@" + c,
|
||||
Pair: p,
|
||||
Asset: asset.Spot,
|
||||
})
|
||||
}
|
||||
}
|
||||
subs, err := b.GenerateSubscriptions()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(subs) == 0 {
|
||||
t.Fatal("unexpected subscription length")
|
||||
assert.NoError(t, err, "GenerateSubscriptions should not error")
|
||||
if assert.Len(t, subs, len(expected), "Should have the correct number of subs") {
|
||||
assert.ElementsMatch(t, subs, expected, "Should get the correct subscriptions")
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannelName(t *testing.T) {
|
||||
_, err := channelName(&subscription.Subscription{Channel: "Wobbegongs"})
|
||||
assert.ErrorIs(t, err, stream.ErrSubscriptionNotSupported, "Invalid channel name should return ErrSubNotSupported")
|
||||
assert.ErrorContains(t, err, "Wobbegong", "Invalid channel name error should contain at least one shark")
|
||||
|
||||
n, err := channelName(&subscription.Subscription{Channel: subscription.TickerChannel})
|
||||
assert.NoError(t, err, "Ticker channel should not error")
|
||||
assert.Equal(t, "ticker", n, "Ticker channel name should be correct")
|
||||
|
||||
n, err = channelName(&subscription.Subscription{Channel: subscription.AllTradesChannel})
|
||||
assert.NoError(t, err, "AllTrades channel should not error")
|
||||
assert.Equal(t, "trade", n, "Trades channel name should be correct")
|
||||
|
||||
n, err = channelName(&subscription.Subscription{Channel: subscription.OrderbookChannel})
|
||||
assert.NoError(t, err, "Orderbook channel should not error")
|
||||
assert.Equal(t, "depth@0s", n, "Orderbook with no update rate should return 0s") // It's not channelName's job to supply defaults
|
||||
|
||||
n, err = channelName(&subscription.Subscription{Channel: subscription.OrderbookChannel, Interval: kline.Interval(time.Second)})
|
||||
assert.NoError(t, err, "Orderbook channel should not error")
|
||||
assert.Equal(t, "depth@1000ms", n, "Orderbook with 1s update rate should 1000ms")
|
||||
|
||||
n, err = channelName(&subscription.Subscription{Channel: subscription.OrderbookChannel, Interval: kline.HundredMilliseconds})
|
||||
assert.NoError(t, err, "Orderbook channel should not error")
|
||||
assert.Equal(t, "depth@100ms", n, "Orderbook with update rate should return it in the depth channel name")
|
||||
|
||||
n, err = channelName(&subscription.Subscription{Channel: subscription.OrderbookChannel, Interval: kline.HundredMilliseconds, Levels: 5})
|
||||
assert.NoError(t, err, "Orderbook channel should not error")
|
||||
assert.Equal(t, "depth@5@100ms", n, "Orderbook with Level should return it in the depth channel name")
|
||||
|
||||
n, err = channelName(&subscription.Subscription{Channel: subscription.CandlesChannel, Interval: kline.FifteenMin})
|
||||
assert.NoError(t, err, "Candles channel should not error")
|
||||
assert.Equal(t, "kline_15m", n, "Candles with interval should return it in the depth channel name")
|
||||
|
||||
n, err = channelName(&subscription.Subscription{Channel: subscription.CandlesChannel})
|
||||
assert.NoError(t, err, "Candles channel should not error")
|
||||
assert.Equal(t, "kline_0s", n, "Candles with no interval should return 0s") // It's not channelName's job to supply defaults
|
||||
}
|
||||
|
||||
var websocketDepthUpdate = []byte(`{"E":1608001030784,"U":7145637266,"a":[["19455.19000000","0.59490200"],["19455.37000000","0.00000000"],["19456.11000000","0.00000000"],["19456.16000000","0.00000000"],["19458.67000000","0.06400000"],["19460.73000000","0.05139800"],["19461.43000000","0.00000000"],["19464.59000000","0.00000000"],["19466.03000000","0.45000000"],["19466.36000000","0.00000000"],["19508.67000000","0.00000000"],["19572.96000000","0.00217200"],["24386.00000000","0.00256600"]],"b":[["19455.18000000","2.94649200"],["19453.15000000","0.01233600"],["19451.18000000","0.00000000"],["19446.85000000","0.11427900"],["19446.74000000","0.00000000"],["19446.73000000","0.00000000"],["19444.45000000","0.14937800"],["19426.75000000","0.00000000"],["19416.36000000","0.36052100"]],"e":"depthUpdate","s":"BTCUSDT","u":7145637297}`)
|
||||
|
||||
func TestProcessUpdate(t *testing.T) {
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/stream"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/subscription"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/trade"
|
||||
"github.com/thrasher-corp/gocryptotrader/log"
|
||||
@@ -548,35 +549,59 @@ func (b *Binance) UpdateLocalBuffer(wsdp *WebsocketDepthStream) (bool, error) {
|
||||
}
|
||||
|
||||
// GenerateSubscriptions generates the default subscription set
|
||||
func (b *Binance) GenerateSubscriptions() ([]stream.ChannelSubscription, error) {
|
||||
var channels = []string{"@ticker", "@trade", "@kline_1m", "@depth@100ms"}
|
||||
var subscriptions []stream.ChannelSubscription
|
||||
assets := b.GetAssetTypes(true)
|
||||
for x := range assets {
|
||||
if assets[x] == asset.Spot {
|
||||
pairs, err := b.GetEnabledPairs(assets[x])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for y := range pairs {
|
||||
for z := range channels {
|
||||
lp := pairs[y].Lower()
|
||||
lp.Delimiter = ""
|
||||
subscriptions = append(subscriptions, stream.ChannelSubscription{
|
||||
Channel: lp.String() + channels[z],
|
||||
Currency: pairs[y],
|
||||
Asset: assets[x],
|
||||
})
|
||||
}
|
||||
}
|
||||
func (b *Binance) GenerateSubscriptions() ([]subscription.Subscription, error) {
|
||||
var channels = make([]string, 0, len(b.Features.Subscriptions))
|
||||
for i := range b.Features.Subscriptions {
|
||||
name, err := channelName(b.Features.Subscriptions[i])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
channels = append(channels, name)
|
||||
}
|
||||
var subscriptions []subscription.Subscription
|
||||
pairs, err := b.GetEnabledPairs(asset.Spot)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for y := range pairs {
|
||||
for z := range channels {
|
||||
lp := pairs[y].Lower()
|
||||
lp.Delimiter = ""
|
||||
subscriptions = append(subscriptions, subscription.Subscription{
|
||||
Channel: lp.String() + "@" + channels[z],
|
||||
Pair: pairs[y],
|
||||
Asset: asset.Spot,
|
||||
})
|
||||
}
|
||||
}
|
||||
return subscriptions, nil
|
||||
}
|
||||
|
||||
// channelName converts a Subscription Config into binance format channel suffix
|
||||
func channelName(s *subscription.Subscription) (string, error) {
|
||||
name, ok := subscriptionNames[s.Channel]
|
||||
if !ok {
|
||||
return name, fmt.Errorf("%w: %s", stream.ErrSubscriptionNotSupported, s.Channel)
|
||||
}
|
||||
|
||||
switch s.Channel {
|
||||
case subscription.OrderbookChannel:
|
||||
if s.Levels != 0 {
|
||||
name += "@" + strconv.Itoa(s.Levels)
|
||||
}
|
||||
if s.Interval.Duration() == time.Second {
|
||||
name += "@1000ms"
|
||||
} else {
|
||||
name += "@" + s.Interval.Short()
|
||||
}
|
||||
case subscription.CandlesChannel:
|
||||
name += "_" + s.Interval.Short()
|
||||
}
|
||||
return name, nil
|
||||
}
|
||||
|
||||
// Subscribe subscribes to a set of channels
|
||||
func (b *Binance) Subscribe(channelsToSubscribe []stream.ChannelSubscription) error {
|
||||
func (b *Binance) Subscribe(channelsToSubscribe []subscription.Subscription) error {
|
||||
payload := WsPayload{
|
||||
Method: "SUBSCRIBE",
|
||||
}
|
||||
@@ -601,7 +626,7 @@ func (b *Binance) Subscribe(channelsToSubscribe []stream.ChannelSubscription) er
|
||||
}
|
||||
|
||||
// Unsubscribe unsubscribes from a set of channels
|
||||
func (b *Binance) Unsubscribe(channelsToUnsubscribe []stream.ChannelSubscription) error {
|
||||
func (b *Binance) Unsubscribe(channelsToUnsubscribe []subscription.Subscription) error {
|
||||
payload := WsPayload{
|
||||
Method: "UNSUBSCRIBE",
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ import (
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/stream"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/stream/buffer"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/subscription"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/trade"
|
||||
"github.com/thrasher-corp/gocryptotrader/log"
|
||||
@@ -211,6 +212,12 @@ func (b *Binance) SetDefaults() {
|
||||
GlobalResultLimit: 1000,
|
||||
},
|
||||
},
|
||||
Subscriptions: []*subscription.Subscription{
|
||||
{Enabled: true, Channel: subscription.TickerChannel},
|
||||
{Enabled: true, Channel: subscription.AllTradesChannel},
|
||||
{Enabled: true, Channel: subscription.CandlesChannel, Interval: kline.OneMin},
|
||||
{Enabled: true, Channel: subscription.OrderbookChannel, Interval: kline.HundredMilliseconds},
|
||||
},
|
||||
}
|
||||
|
||||
b.Requester, err = request.New(b.Name,
|
||||
|
||||
Reference in New Issue
Block a user