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:
Gareth Kirwan
2024-01-24 05:54:07 +01:00
committed by GitHub
parent 301551ac20
commit e007f69f7c
67 changed files with 3705 additions and 3167 deletions

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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",
}

View File

@@ -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,

View File

@@ -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"
@@ -539,9 +540,9 @@ func (bi *Binanceus) UpdateLocalBuffer(wsdp *WebsocketDepthStream) (bool, error)
}
// GenerateSubscriptions generates the default subscription set
func (bi *Binanceus) GenerateSubscriptions() ([]stream.ChannelSubscription, error) {
func (bi *Binanceus) GenerateSubscriptions() ([]subscription.Subscription, error) {
var channels = []string{"@ticker", "@trade", "@kline_1m", "@depth@100ms"}
var subscriptions []stream.ChannelSubscription
var subscriptions []subscription.Subscription
pairs, err := bi.GetEnabledPairs(asset.Spot)
if err != nil {
@@ -557,10 +558,10 @@ subs:
log.Warnf(log.WebsocketMgr, "BinanceUS has 1024 subscription limit, only subscribing within limit. Requested %v", len(pairs)*len(channels))
break subs
}
subscriptions = append(subscriptions, stream.ChannelSubscription{
Channel: lp.String() + channels[z],
Currency: pairs[y],
Asset: asset.Spot,
subscriptions = append(subscriptions, subscription.Subscription{
Channel: lp.String() + channels[z],
Pair: pairs[y],
Asset: asset.Spot,
})
}
}
@@ -569,7 +570,7 @@ subs:
}
// Subscribe subscribes to a set of channels
func (bi *Binanceus) Subscribe(channelsToSubscribe []stream.ChannelSubscription) error {
func (bi *Binanceus) Subscribe(channelsToSubscribe []subscription.Subscription) error {
payload := WebsocketPayload{
Method: "SUBSCRIBE",
}
@@ -594,7 +595,7 @@ func (bi *Binanceus) Subscribe(channelsToSubscribe []stream.ChannelSubscription)
}
// Unsubscribe unsubscribes from a set of channels
func (bi *Binanceus) Unsubscribe(channelsToUnsubscribe []stream.ChannelSubscription) error {
func (bi *Binanceus) Unsubscribe(channelsToUnsubscribe []subscription.Subscription) error {
payload := WebsocketPayload{
Method: "UNSUBSCRIBE",
}

View File

@@ -101,9 +101,9 @@ const (
bitfinexChecksumFlag = 131072
bitfinexWsSequenceFlag = 65536
// CandlesTimeframeKey configures the timeframe in stream.ChannelSubscription.Params
// CandlesTimeframeKey configures the timeframe in subscription.Subscription.Params
CandlesTimeframeKey = "_timeframe"
// CandlesPeriodKey configures the aggregated period in stream.ChannelSubscription.Params
// CandlesPeriodKey configures the aggregated period in subscription.Subscription.Params
CandlesPeriodKey = "_period"
)

File diff suppressed because one or more lines are too long

View File

@@ -23,6 +23,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"
@@ -505,7 +506,7 @@ func (b *Bitfinex) handleWSSubscribed(respRaw []byte) error {
chanID, err := jsonparser.GetInt(respRaw, "chanId")
if err != nil {
return fmt.Errorf("%w: %w 'chanId': %w; Channel: %s Pair: %s", stream.ErrSubscriptionFailure, errParsingWSField, err, c.Channel, c.Currency)
return fmt.Errorf("%w: %w 'chanId': %w; Channel: %s Pair: %s", stream.ErrSubscriptionFailure, errParsingWSField, err, c.Channel, c.Pair)
}
// Note: chanID's int type avoids conflicts with the string type subID key because of the type difference
@@ -515,7 +516,7 @@ func (b *Bitfinex) handleWSSubscribed(respRaw []byte) error {
b.Websocket.AddSuccessfulSubscriptions(*c)
if b.Verbose {
log.Debugf(log.ExchangeSys, "%s Subscribed to Channel: %s Pair: %s ChannelID: %d\n", b.Name, c.Channel, c.Currency, chanID)
log.Debugf(log.ExchangeSys, "%s Subscribed to Channel: %s Pair: %s ChannelID: %d\n", b.Name, c.Channel, c.Pair, chanID)
}
if !b.Websocket.Match.IncomingWithData("subscribe:"+subID, respRaw) {
return fmt.Errorf("%v channel subscribe listener not found", subID)
@@ -523,7 +524,7 @@ func (b *Bitfinex) handleWSSubscribed(respRaw []byte) error {
return nil
}
func (b *Bitfinex) handleWSChannelUpdate(c *stream.ChannelSubscription, eventType string, d []interface{}) error {
func (b *Bitfinex) handleWSChannelUpdate(c *subscription.Subscription, eventType string, d []interface{}) error {
if eventType == wsChecksum {
return b.handleWSChecksum(c, d)
}
@@ -546,7 +547,7 @@ func (b *Bitfinex) handleWSChannelUpdate(c *stream.ChannelSubscription, eventTyp
return fmt.Errorf("%s unhandled channel update: %s", b.Name, c.Channel)
}
func (b *Bitfinex) handleWSChecksum(c *stream.ChannelSubscription, d []interface{}) error {
func (b *Bitfinex) handleWSChecksum(c *subscription.Subscription, d []interface{}) error {
var token int
if f, ok := d[2].(float64); !ok {
return common.GetTypeAssertError("float64", d[2], "checksum")
@@ -577,7 +578,7 @@ func (b *Bitfinex) handleWSChecksum(c *stream.ChannelSubscription, d []interface
return nil
}
func (b *Bitfinex) handleWSBookUpdate(c *stream.ChannelSubscription, d []interface{}) error {
func (b *Bitfinex) handleWSBookUpdate(c *subscription.Subscription, d []interface{}) error {
var newOrderbook []WebsocketBook
obSnapBundle, ok := d[1].([]interface{})
if !ok {
@@ -631,7 +632,7 @@ func (b *Bitfinex) handleWSBookUpdate(c *stream.ChannelSubscription, d []interfa
Amount: rateAmount})
}
}
if err := b.WsInsertSnapshot(c.Currency, c.Asset, newOrderbook, fundingRate); err != nil {
if err := b.WsInsertSnapshot(c.Pair, c.Asset, newOrderbook, fundingRate); err != nil {
return fmt.Errorf("inserting snapshot error: %s",
err)
}
@@ -663,7 +664,7 @@ func (b *Bitfinex) handleWSBookUpdate(c *stream.ChannelSubscription, d []interfa
Amount: amountRate})
}
if err := b.WsUpdateOrderbook(c, c.Currency, c.Asset, newOrderbook, int64(sequenceNo), fundingRate); err != nil {
if err := b.WsUpdateOrderbook(c, c.Pair, c.Asset, newOrderbook, int64(sequenceNo), fundingRate); err != nil {
return fmt.Errorf("updating orderbook error: %s",
err)
}
@@ -672,7 +673,7 @@ func (b *Bitfinex) handleWSBookUpdate(c *stream.ChannelSubscription, d []interfa
return nil
}
func (b *Bitfinex) handleWSCandleUpdate(c *stream.ChannelSubscription, d []interface{}) error {
func (b *Bitfinex) handleWSCandleUpdate(c *subscription.Subscription, d []interface{}) error {
candleBundle, ok := d[1].([]interface{})
if !ok || len(candleBundle) == 0 {
return nil
@@ -711,7 +712,7 @@ func (b *Bitfinex) handleWSCandleUpdate(c *stream.ChannelSubscription, d []inter
}
klineData.Exchange = b.Name
klineData.AssetType = c.Asset
klineData.Pair = c.Currency
klineData.Pair = c.Pair
b.Websocket.DataHandler <- klineData
}
case float64:
@@ -740,13 +741,13 @@ func (b *Bitfinex) handleWSCandleUpdate(c *stream.ChannelSubscription, d []inter
}
klineData.Exchange = b.Name
klineData.AssetType = c.Asset
klineData.Pair = c.Currency
klineData.Pair = c.Pair
b.Websocket.DataHandler <- klineData
}
return nil
}
func (b *Bitfinex) handleWSTickerUpdate(c *stream.ChannelSubscription, d []interface{}) error {
func (b *Bitfinex) handleWSTickerUpdate(c *subscription.Subscription, d []interface{}) error {
tickerData, ok := d[1].([]interface{})
if !ok {
return errors.New("type assertion for tickerData")
@@ -754,7 +755,7 @@ func (b *Bitfinex) handleWSTickerUpdate(c *stream.ChannelSubscription, d []inter
t := &ticker.Price{
AssetType: c.Asset,
Pair: c.Currency,
Pair: c.Pair,
ExchangeName: b.Name,
}
@@ -819,7 +820,7 @@ func (b *Bitfinex) handleWSTickerUpdate(c *stream.ChannelSubscription, d []inter
return nil
}
func (b *Bitfinex) handleWSTradesUpdate(c *stream.ChannelSubscription, eventType string, d []interface{}) error {
func (b *Bitfinex) handleWSTradesUpdate(c *subscription.Subscription, eventType string, d []interface{}) error {
if !b.IsSaveTradeDataEnabled() {
return nil
}
@@ -935,7 +936,7 @@ func (b *Bitfinex) handleWSTradesUpdate(c *stream.ChannelSubscription, eventType
}
trades[i] = trade.Data{
TID: strconv.FormatInt(tradeHolder[i].ID, 10),
CurrencyPair: c.Currency,
CurrencyPair: c.Pair,
Timestamp: time.UnixMilli(tradeHolder[i].Timestamp),
Price: price,
Amount: newAmount,
@@ -1508,7 +1509,7 @@ func (b *Bitfinex) WsInsertSnapshot(p currency.Pair, assetType asset.Item, books
// WsUpdateOrderbook updates the orderbook list, removing and adding to the
// orderbook sides
func (b *Bitfinex) WsUpdateOrderbook(c *stream.ChannelSubscription, p currency.Pair, assetType asset.Item, book []WebsocketBook, sequenceNo int64, fundingRate bool) error {
func (b *Bitfinex) WsUpdateOrderbook(c *subscription.Subscription, p currency.Pair, assetType asset.Item, book []WebsocketBook, sequenceNo int64, fundingRate bool) error {
orderbookUpdate := orderbook.Update{
Asset: assetType,
Pair: p,
@@ -1602,8 +1603,8 @@ func (b *Bitfinex) WsUpdateOrderbook(c *stream.ChannelSubscription, p currency.P
// resubOrderbook resubscribes the orderbook after a consistency error, probably a failed checksum,
// which forces a fresh snapshot. If we don't do this the orderbook will keep erroring and drifting.
// Flushing the orderbook happens immediately, but the ReSub itself is a go routine to avoid blocking the WS data channel
func (b *Bitfinex) resubOrderbook(c *stream.ChannelSubscription) {
if err := b.Websocket.Orderbook.FlushOrderbook(c.Currency, c.Asset); err != nil {
func (b *Bitfinex) resubOrderbook(c *subscription.Subscription) {
if err := b.Websocket.Orderbook.FlushOrderbook(c.Pair, c.Asset); err != nil {
log.Errorf(log.ExchangeSys, "%s error flushing orderbook: %v", b.Name, err)
}
@@ -1616,10 +1617,10 @@ func (b *Bitfinex) resubOrderbook(c *stream.ChannelSubscription) {
}
// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions()
func (b *Bitfinex) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, error) {
func (b *Bitfinex) GenerateDefaultSubscriptions() ([]subscription.Subscription, error) {
var channels = []string{wsBook, wsTrades, wsTicker, wsCandles}
var subscriptions []stream.ChannelSubscription
var subscriptions []subscription.Subscription
assets := b.GetAssetTypes(true)
for i := range assets {
if !b.IsAssetWebsocketSupported(assets[i]) {
@@ -1642,11 +1643,11 @@ func (b *Bitfinex) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription,
params[CandlesPeriodKey] = "30"
}
subscriptions = append(subscriptions, stream.ChannelSubscription{
Channel: channels[j],
Currency: enabledPairs[k],
Params: params,
Asset: assets[i],
subscriptions = append(subscriptions, subscription.Subscription{
Channel: channels[j],
Pair: enabledPairs[k],
Params: params,
Asset: assets[i],
})
}
}
@@ -1664,47 +1665,26 @@ func (b *Bitfinex) ConfigureWS() error {
}
// Subscribe sends a websocket message to receive data from channels
func (b *Bitfinex) Subscribe(channels []stream.ChannelSubscription) error {
return b.parallelChanOp(channels, b.subscribeToChan)
func (b *Bitfinex) Subscribe(channels []subscription.Subscription) error {
return b.ParallelChanOp(channels, b.subscribeToChan, 1)
}
// Unsubscribe sends a websocket message to stop receiving data from channels
func (b *Bitfinex) Unsubscribe(channels []stream.ChannelSubscription) error {
return b.parallelChanOp(channels, b.unsubscribeFromChan)
}
// parallelChanOp performs a single method call in parallel across streams and waits to return any errors
func (b *Bitfinex) parallelChanOp(channels []stream.ChannelSubscription, m func(*stream.ChannelSubscription) error) error {
wg := sync.WaitGroup{}
wg.Add(len(channels))
errC := make(chan error, len(channels))
for i := range channels {
go func(c *stream.ChannelSubscription) {
defer wg.Done()
if err := m(c); err != nil {
errC <- err
}
}(&channels[i])
}
wg.Wait()
close(errC)
var errs error
for err := range errC {
errs = common.AppendError(errs, err)
}
return errs
func (b *Bitfinex) Unsubscribe(channels []subscription.Subscription) error {
return b.ParallelChanOp(channels, b.unsubscribeFromChan, 1)
}
// subscribeToChan handles a single subscription and parses the result
// on success it adds the subscription to the websocket
func (b *Bitfinex) subscribeToChan(c *stream.ChannelSubscription) error {
req, err := subscribeReq(c)
func (b *Bitfinex) subscribeToChan(chans []subscription.Subscription) error {
if len(chans) != 1 {
return errors.New("subscription batching limited to 1")
}
c := chans[0]
req, err := subscribeReq(&c)
if err != nil {
return fmt.Errorf("%w: %w; Channel: %s Pair: %s", stream.ErrSubscriptionFailure, err, c.Channel, c.Currency)
return fmt.Errorf("%w: %w; Channel: %s Pair: %s", stream.ErrSubscriptionFailure, err, c.Channel, c.Pair)
}
// subId is a single round-trip identifier that provides linking sub requests to chanIDs
@@ -1716,22 +1696,22 @@ func (b *Bitfinex) subscribeToChan(c *stream.ChannelSubscription) error {
// Otherwise we might drop the first messages after the subscribed resp
c.Key = subID // Note subID string type avoids conflicts with later chanID key
c.State = stream.ChannelSubscribing
err = b.Websocket.AddSubscription(c)
c.State = subscription.SubscribingState
err = b.Websocket.AddSubscription(&c)
if err != nil {
return fmt.Errorf("%w Channel: %s Pair: %s Error: %w", stream.ErrSubscriptionFailure, c.Channel, c.Currency, err)
return fmt.Errorf("%w Channel: %s Pair: %s Error: %w", stream.ErrSubscriptionFailure, c.Channel, c.Pair, err)
}
// Always remove the temporary subscription keyed by subID
defer b.Websocket.RemoveSubscriptions(*c)
defer b.Websocket.RemoveSubscriptions(c)
respRaw, err := b.Websocket.Conn.SendMessageReturnResponse("subscribe:"+subID, req)
if err != nil {
return fmt.Errorf("%w: %w; Channel: %s Pair: %s", stream.ErrSubscriptionFailure, err, c.Channel, c.Currency)
return fmt.Errorf("%w: %w; Channel: %s Pair: %s", stream.ErrSubscriptionFailure, err, c.Channel, c.Pair)
}
if err = b.getErrResp(respRaw); err != nil {
wErr := fmt.Errorf("%w: %w; Channel: %s Pair: %s", stream.ErrSubscriptionFailure, err, c.Channel, c.Currency)
wErr := fmt.Errorf("%w: %w; Channel: %s Pair: %s", stream.ErrSubscriptionFailure, err, c.Channel, c.Pair)
b.Websocket.DataHandler <- wErr
return wErr
}
@@ -1740,7 +1720,7 @@ func (b *Bitfinex) subscribeToChan(c *stream.ChannelSubscription) error {
}
// subscribeReq returns a map of request params for subscriptions
func subscribeReq(c *stream.ChannelSubscription) (map[string]interface{}, error) {
func subscribeReq(c *subscription.Subscription) (map[string]interface{}, error) {
req := map[string]interface{}{
"event": "subscribe",
"channel": c.Channel,
@@ -1763,13 +1743,13 @@ func subscribeReq(c *stream.ChannelSubscription) (map[string]interface{}, error)
prefix = "f"
}
needsDelimiter := c.Currency.Len() > 6
needsDelimiter := c.Pair.Len() > 6
var formattedPair string
if needsDelimiter {
formattedPair = c.Currency.Format(currency.PairFormat{Uppercase: true, Delimiter: ":"}).String()
formattedPair = c.Pair.Format(currency.PairFormat{Uppercase: true, Delimiter: ":"}).String()
} else {
formattedPair = currency.PairFormat{Uppercase: true}.Format(c.Currency)
formattedPair = currency.PairFormat{Uppercase: true}.Format(c.Pair)
}
if c.Channel == wsCandles {
@@ -1796,7 +1776,11 @@ func subscribeReq(c *stream.ChannelSubscription) (map[string]interface{}, error)
}
// unsubscribeFromChan sends a websocket message to stop receiving data from a channel
func (b *Bitfinex) unsubscribeFromChan(c *stream.ChannelSubscription) error {
func (b *Bitfinex) unsubscribeFromChan(chans []subscription.Subscription) error {
if len(chans) != 1 {
return errors.New("subscription batching limited to 1")
}
c := chans[0]
chanID, ok := c.Key.(int)
if !ok {
return common.GetTypeAssertError("int", c.Key, "chanID")
@@ -1818,7 +1802,7 @@ func (b *Bitfinex) unsubscribeFromChan(c *stream.ChannelSubscription) error {
return wErr
}
b.Websocket.RemoveSubscriptions(*c)
b.Websocket.RemoveSubscriptions(c)
return nil
}

View File

@@ -10,6 +10,7 @@ import (
"github.com/gorilla/websocket"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"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"
)
@@ -167,9 +168,9 @@ func (b *Bithumb) wsHandleData(respRaw []byte) error {
}
// GenerateSubscriptions generates the default subscription set
func (b *Bithumb) GenerateSubscriptions() ([]stream.ChannelSubscription, error) {
func (b *Bithumb) GenerateSubscriptions() ([]subscription.Subscription, error) {
var channels = []string{"ticker", "transaction", "orderbookdepth"}
var subscriptions []stream.ChannelSubscription
var subscriptions []subscription.Subscription
pairs, err := b.GetEnabledPairs(asset.Spot)
if err != nil {
return nil, err
@@ -182,10 +183,10 @@ func (b *Bithumb) GenerateSubscriptions() ([]stream.ChannelSubscription, error)
for x := range pairs {
for y := range channels {
subscriptions = append(subscriptions, stream.ChannelSubscription{
Channel: channels[y],
Currency: pairs[x].Format(pFmt),
Asset: asset.Spot,
subscriptions = append(subscriptions, subscription.Subscription{
Channel: channels[y],
Pair: pairs[x].Format(pFmt),
Asset: asset.Spot,
})
}
}
@@ -193,7 +194,7 @@ func (b *Bithumb) GenerateSubscriptions() ([]stream.ChannelSubscription, error)
}
// Subscribe subscribes to a set of channels
func (b *Bithumb) Subscribe(channelsToSubscribe []stream.ChannelSubscription) error {
func (b *Bithumb) Subscribe(channelsToSubscribe []subscription.Subscription) error {
subs := make(map[string]*WsSubscribe)
for i := range channelsToSubscribe {
s, ok := subs[channelsToSubscribe[i].Channel]
@@ -203,7 +204,7 @@ func (b *Bithumb) Subscribe(channelsToSubscribe []stream.ChannelSubscription) er
}
subs[channelsToSubscribe[i].Channel] = s
}
s.Symbols = append(s.Symbols, channelsToSubscribe[i].Currency)
s.Symbols = append(s.Symbols, channelsToSubscribe[i].Pair)
}
tSub, ok := subs["ticker"]

View File

@@ -17,6 +17,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/trade"
"github.com/thrasher-corp/gocryptotrader/log"
)
@@ -544,9 +545,9 @@ func (b *Bitmex) processOrderbook(data []OrderBookL2, action string, p currency.
}
// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions()
func (b *Bitmex) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, error) {
func (b *Bitmex) GenerateDefaultSubscriptions() ([]subscription.Subscription, error) {
channels := []string{bitmexWSOrderbookL2, bitmexWSTrade}
subscriptions := []stream.ChannelSubscription{
subscriptions := []subscription.Subscription{
{
Channel: bitmexWSAnnouncement,
},
@@ -568,10 +569,10 @@ func (b *Bitmex) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, e
// There are no L2 orderbook for index assets
continue
}
subscriptions = append(subscriptions, stream.ChannelSubscription{
Channel: channels[z] + ":" + pFmt.Format(contracts[y]),
Currency: contracts[y],
Asset: assets[x],
subscriptions = append(subscriptions, subscription.Subscription{
Channel: channels[z] + ":" + pFmt.Format(contracts[y]),
Pair: contracts[y],
Asset: assets[x],
})
}
}
@@ -580,7 +581,7 @@ func (b *Bitmex) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, e
}
// GenerateAuthenticatedSubscriptions Adds authenticated subscriptions to websocket to be handled by ManageSubscriptions()
func (b *Bitmex) GenerateAuthenticatedSubscriptions() ([]stream.ChannelSubscription, error) {
func (b *Bitmex) GenerateAuthenticatedSubscriptions() ([]subscription.Subscription, error) {
if !b.Websocket.CanUseAuthenticatedEndpoints() {
return nil, nil
}
@@ -596,7 +597,7 @@ func (b *Bitmex) GenerateAuthenticatedSubscriptions() ([]stream.ChannelSubscript
channels := []string{bitmexWSExecution,
bitmexWSPosition,
}
subscriptions := []stream.ChannelSubscription{
subscriptions := []subscription.Subscription{
{
Channel: bitmexWSAffiliate,
},
@@ -618,10 +619,10 @@ func (b *Bitmex) GenerateAuthenticatedSubscriptions() ([]stream.ChannelSubscript
}
for i := range channels {
for j := range contracts {
subscriptions = append(subscriptions, stream.ChannelSubscription{
Channel: channels[i] + ":" + pFmt.Format(contracts[j]),
Currency: contracts[j],
Asset: asset.PerpetualContract,
subscriptions = append(subscriptions, subscription.Subscription{
Channel: channels[i] + ":" + pFmt.Format(contracts[j]),
Pair: contracts[j],
Asset: asset.PerpetualContract,
})
}
}
@@ -629,7 +630,7 @@ func (b *Bitmex) GenerateAuthenticatedSubscriptions() ([]stream.ChannelSubscript
}
// Subscribe subscribes to a websocket channel
func (b *Bitmex) Subscribe(channelsToSubscribe []stream.ChannelSubscription) error {
func (b *Bitmex) Subscribe(channelsToSubscribe []subscription.Subscription) error {
var subscriber WebsocketRequest
subscriber.Command = "subscribe"
for i := range channelsToSubscribe {
@@ -645,7 +646,7 @@ func (b *Bitmex) Subscribe(channelsToSubscribe []stream.ChannelSubscription) err
}
// Unsubscribe sends a websocket message to stop receiving data from the channel
func (b *Bitmex) Unsubscribe(channelsToUnsubscribe []stream.ChannelSubscription) error {
func (b *Bitmex) Unsubscribe(channelsToUnsubscribe []subscription.Subscription) error {
var unsubscriber WebsocketRequest
unsubscriber.Command = "unsubscribe"

View File

@@ -18,6 +18,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/trade"
"github.com/thrasher-corp/gocryptotrader/log"
)
@@ -230,30 +231,30 @@ func (b *Bitstamp) handleWSOrder(wsResp *websocketResponse, msg []byte) error {
return nil
}
func (b *Bitstamp) generateDefaultSubscriptions() ([]stream.ChannelSubscription, error) {
func (b *Bitstamp) generateDefaultSubscriptions() ([]subscription.Subscription, error) {
enabledCurrencies, err := b.GetEnabledPairs(asset.Spot)
if err != nil {
return nil, err
}
var subscriptions []stream.ChannelSubscription
var subscriptions []subscription.Subscription
for i := range enabledCurrencies {
p, err := b.FormatExchangeCurrency(enabledCurrencies[i], asset.Spot)
if err != nil {
return nil, err
}
for j := range defaultSubChannels {
subscriptions = append(subscriptions, stream.ChannelSubscription{
Channel: defaultSubChannels[j] + "_" + p.String(),
Asset: asset.Spot,
Currency: p,
subscriptions = append(subscriptions, subscription.Subscription{
Channel: defaultSubChannels[j] + "_" + p.String(),
Asset: asset.Spot,
Pair: p,
})
}
if b.Websocket.CanUseAuthenticatedEndpoints() {
for j := range defaultAuthSubChannels {
subscriptions = append(subscriptions, stream.ChannelSubscription{
Channel: defaultAuthSubChannels[j] + "_" + p.String(),
Asset: asset.Spot,
Currency: p,
subscriptions = append(subscriptions, subscription.Subscription{
Channel: defaultAuthSubChannels[j] + "_" + p.String(),
Asset: asset.Spot,
Pair: p,
Params: map[string]interface{}{
"auth": struct{}{},
},
@@ -265,7 +266,7 @@ func (b *Bitstamp) generateDefaultSubscriptions() ([]stream.ChannelSubscription,
}
// Subscribe sends a websocket message to receive data from the channel
func (b *Bitstamp) Subscribe(channelsToSubscribe []stream.ChannelSubscription) error {
func (b *Bitstamp) Subscribe(channelsToSubscribe []subscription.Subscription) error {
var errs error
var auth *WebsocketAuthResponse
@@ -303,7 +304,7 @@ func (b *Bitstamp) Subscribe(channelsToSubscribe []stream.ChannelSubscription) e
}
// Unsubscribe sends a websocket message to stop receiving data from the channel
func (b *Bitstamp) Unsubscribe(channelsToUnsubscribe []stream.ChannelSubscription) error {
func (b *Bitstamp) Unsubscribe(channelsToUnsubscribe []subscription.Subscription) error {
var errs error
for i := range channelsToUnsubscribe {
req := websocketEventRequest{

View File

@@ -19,6 +19,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"
@@ -324,26 +325,26 @@ func (b *BTCMarkets) wsHandleData(respRaw []byte) error {
return nil
}
func (b *BTCMarkets) generateDefaultSubscriptions() ([]stream.ChannelSubscription, error) {
func (b *BTCMarkets) generateDefaultSubscriptions() ([]subscription.Subscription, error) {
var channels = []string{wsOB, tick, tradeEndPoint}
enabledCurrencies, err := b.GetEnabledPairs(asset.Spot)
if err != nil {
return nil, err
}
var subscriptions []stream.ChannelSubscription
var subscriptions []subscription.Subscription
for i := range channels {
for j := range enabledCurrencies {
subscriptions = append(subscriptions, stream.ChannelSubscription{
Channel: channels[i],
Currency: enabledCurrencies[j],
Asset: asset.Spot,
subscriptions = append(subscriptions, subscription.Subscription{
Channel: channels[i],
Pair: enabledCurrencies[j],
Asset: asset.Spot,
})
}
}
if b.Websocket.CanUseAuthenticatedEndpoints() {
for i := range authChannels {
subscriptions = append(subscriptions, stream.ChannelSubscription{
subscriptions = append(subscriptions, subscription.Subscription{
Channel: authChannels[i],
})
}
@@ -352,7 +353,7 @@ func (b *BTCMarkets) generateDefaultSubscriptions() ([]stream.ChannelSubscriptio
}
// Subscribe sends a websocket message to receive data from the channel
func (b *BTCMarkets) Subscribe(subs []stream.ChannelSubscription) error {
func (b *BTCMarkets) Subscribe(subs []subscription.Subscription) error {
var payload WsSubscribe
if len(subs) > 1 {
// TODO: Expand this to stream package as this assumes that we are doing
@@ -369,10 +370,10 @@ func (b *BTCMarkets) Subscribe(subs []stream.ChannelSubscription) error {
authenticate = true
}
payload.Channels = append(payload.Channels, subs[i].Channel)
if subs[i].Currency.IsEmpty() {
if subs[i].Pair.IsEmpty() {
continue
}
pair := subs[i].Currency.String()
pair := subs[i].Pair.String()
if common.StringDataCompare(payload.MarketIDs, pair) {
continue
}
@@ -407,18 +408,18 @@ func (b *BTCMarkets) Subscribe(subs []stream.ChannelSubscription) error {
}
// Unsubscribe sends a websocket message to manage and remove a subscription.
func (b *BTCMarkets) Unsubscribe(subs []stream.ChannelSubscription) error {
func (b *BTCMarkets) Unsubscribe(subs []subscription.Subscription) error {
payload := WsSubscribe{
MessageType: removeSubscription,
ClientType: clientType,
}
for i := range subs {
payload.Channels = append(payload.Channels, subs[i].Channel)
if subs[i].Currency.IsEmpty() {
if subs[i].Pair.IsEmpty() {
continue
}
pair := subs[i].Currency.String()
pair := subs[i].Pair.String()
if common.StringDataCompare(payload.MarketIDs, pair) {
continue
}
@@ -436,10 +437,10 @@ func (b *BTCMarkets) Unsubscribe(subs []stream.ChannelSubscription) error {
// ReSubscribeSpecificOrderbook removes the subscription and the subscribes
// again to fetch a new snapshot in the event of a de-sync event.
func (b *BTCMarkets) ReSubscribeSpecificOrderbook(pair currency.Pair) error {
sub := []stream.ChannelSubscription{{
Channel: wsOB,
Currency: pair,
Asset: asset.Spot,
sub := []subscription.Subscription{{
Channel: wsOB,
Pair: pair,
Asset: asset.Spot,
}}
if err := b.Unsubscribe(sub); err != nil {
return err

View File

@@ -17,6 +17,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/trade"
"github.com/thrasher-corp/gocryptotrader/log"
)
@@ -360,24 +361,24 @@ func (b *BTSE) orderbookFilter(price, amount float64) bool {
}
// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions()
func (b *BTSE) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, error) {
func (b *BTSE) GenerateDefaultSubscriptions() ([]subscription.Subscription, error) {
var channels = []string{"orderBookL2Api:%s_0", "tradeHistory:%s"}
pairs, err := b.GetEnabledPairs(asset.Spot)
if err != nil {
return nil, err
}
var subscriptions []stream.ChannelSubscription
var subscriptions []subscription.Subscription
if b.Websocket.CanUseAuthenticatedEndpoints() {
subscriptions = append(subscriptions, stream.ChannelSubscription{
subscriptions = append(subscriptions, subscription.Subscription{
Channel: "notificationApi",
})
}
for i := range channels {
for j := range pairs {
subscriptions = append(subscriptions, stream.ChannelSubscription{
Channel: fmt.Sprintf(channels[i], pairs[j]),
Currency: pairs[j],
Asset: asset.Spot,
subscriptions = append(subscriptions, subscription.Subscription{
Channel: fmt.Sprintf(channels[i], pairs[j]),
Pair: pairs[j],
Asset: asset.Spot,
})
}
}
@@ -385,7 +386,7 @@ func (b *BTSE) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, err
}
// Subscribe sends a websocket message to receive data from the channel
func (b *BTSE) Subscribe(channelsToSubscribe []stream.ChannelSubscription) error {
func (b *BTSE) Subscribe(channelsToSubscribe []subscription.Subscription) error {
var sub wsSub
sub.Operation = "subscribe"
for i := range channelsToSubscribe {
@@ -400,7 +401,7 @@ func (b *BTSE) Subscribe(channelsToSubscribe []stream.ChannelSubscription) error
}
// Unsubscribe sends a websocket message to stop receiving data from the channel
func (b *BTSE) Unsubscribe(channelsToUnsubscribe []stream.ChannelSubscription) error {
func (b *BTSE) Unsubscribe(channelsToUnsubscribe []subscription.Subscription) error {
var unSub wsSub
unSub.Operation = "unsubscribe"
for i := range channelsToUnsubscribe {

View File

@@ -6,6 +6,7 @@ import (
"github.com/gorilla/websocket"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/stream"
"github.com/thrasher-corp/gocryptotrader/exchanges/subscription"
)
// WsInverseConnect connects to inverse websocket feed
@@ -31,8 +32,8 @@ func (by *Bybit) WsInverseConnect() error {
}
// GenerateInverseDefaultSubscriptions generates default subscription
func (by *Bybit) GenerateInverseDefaultSubscriptions() ([]stream.ChannelSubscription, error) {
var subscriptions []stream.ChannelSubscription
func (by *Bybit) GenerateInverseDefaultSubscriptions() ([]subscription.Subscription, error) {
var subscriptions []subscription.Subscription
var channels = []string{chanOrderbook, chanPublicTrade, chanPublicTicker}
pairs, err := by.GetEnabledPairs(asset.CoinMarginedFutures)
if err != nil {
@@ -41,10 +42,10 @@ func (by *Bybit) GenerateInverseDefaultSubscriptions() ([]stream.ChannelSubscrip
for z := range pairs {
for x := range channels {
subscriptions = append(subscriptions,
stream.ChannelSubscription{
Channel: channels[x],
Currency: pairs[z],
Asset: asset.CoinMarginedFutures,
subscription.Subscription{
Channel: channels[x],
Pair: pairs[z],
Asset: asset.CoinMarginedFutures,
})
}
}
@@ -52,16 +53,16 @@ func (by *Bybit) GenerateInverseDefaultSubscriptions() ([]stream.ChannelSubscrip
}
// InverseSubscribe sends a subscription message to linear public channels.
func (by *Bybit) InverseSubscribe(channelSubscriptions []stream.ChannelSubscription) error {
func (by *Bybit) InverseSubscribe(channelSubscriptions []subscription.Subscription) error {
return by.handleInversePayloadSubscription("subscribe", channelSubscriptions)
}
// InverseUnsubscribe sends an unsubscription messages through linear public channels.
func (by *Bybit) InverseUnsubscribe(channelSubscriptions []stream.ChannelSubscription) error {
func (by *Bybit) InverseUnsubscribe(channelSubscriptions []subscription.Subscription) error {
return by.handleInversePayloadSubscription("unsubscribe", channelSubscriptions)
}
func (by *Bybit) handleInversePayloadSubscription(operation string, channelSubscriptions []stream.ChannelSubscription) error {
func (by *Bybit) handleInversePayloadSubscription(operation string, channelSubscriptions []subscription.Subscription) error {
payloads, err := by.handleSubscriptions(asset.CoinMarginedFutures, operation, channelSubscriptions)
if err != nil {
return err

View File

@@ -8,6 +8,7 @@ import (
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/stream"
"github.com/thrasher-corp/gocryptotrader/exchanges/subscription"
)
// WsLinearConnect connects to linear a websocket feed
@@ -40,8 +41,8 @@ func (by *Bybit) WsLinearConnect() error {
}
// GenerateLinearDefaultSubscriptions generates default subscription
func (by *Bybit) GenerateLinearDefaultSubscriptions() ([]stream.ChannelSubscription, error) {
var subscriptions []stream.ChannelSubscription
func (by *Bybit) GenerateLinearDefaultSubscriptions() ([]subscription.Subscription, error) {
var subscriptions []subscription.Subscription
var channels = []string{chanOrderbook, chanPublicTrade, chanPublicTicker}
pairs, err := by.GetEnabledPairs(asset.USDTMarginedFutures)
if err != nil {
@@ -60,10 +61,10 @@ func (by *Bybit) GenerateLinearDefaultSubscriptions() ([]stream.ChannelSubscript
for p := range linearPairMap[a] {
for x := range channels {
subscriptions = append(subscriptions,
stream.ChannelSubscription{
Channel: channels[x],
Currency: pairs[p],
Asset: a,
subscription.Subscription{
Channel: channels[x],
Pair: pairs[p],
Asset: a,
})
}
}
@@ -72,16 +73,16 @@ func (by *Bybit) GenerateLinearDefaultSubscriptions() ([]stream.ChannelSubscript
}
// LinearSubscribe sends a subscription message to linear public channels.
func (by *Bybit) LinearSubscribe(channelSubscriptions []stream.ChannelSubscription) error {
func (by *Bybit) LinearSubscribe(channelSubscriptions []subscription.Subscription) error {
return by.handleLinearPayloadSubscription("subscribe", channelSubscriptions)
}
// LinearUnsubscribe sends an unsubscription messages through linear public channels.
func (by *Bybit) LinearUnsubscribe(channelSubscriptions []stream.ChannelSubscription) error {
func (by *Bybit) LinearUnsubscribe(channelSubscriptions []subscription.Subscription) error {
return by.handleLinearPayloadSubscription("unsubscribe", channelSubscriptions)
}
func (by *Bybit) handleLinearPayloadSubscription(operation string, channelSubscriptions []stream.ChannelSubscription) error {
func (by *Bybit) handleLinearPayloadSubscription(operation string, channelSubscriptions []subscription.Subscription) error {
payloads, err := by.handleSubscriptions(asset.USDTMarginedFutures, operation, channelSubscriptions)
if err != nil {
return err

View File

@@ -8,6 +8,7 @@ import (
"github.com/gorilla/websocket"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/stream"
"github.com/thrasher-corp/gocryptotrader/exchanges/subscription"
)
// WsOptionsConnect connects to options a websocket feed
@@ -38,8 +39,8 @@ func (by *Bybit) WsOptionsConnect() error {
}
// GenerateOptionsDefaultSubscriptions generates default subscription
func (by *Bybit) GenerateOptionsDefaultSubscriptions() ([]stream.ChannelSubscription, error) {
var subscriptions []stream.ChannelSubscription
func (by *Bybit) GenerateOptionsDefaultSubscriptions() ([]subscription.Subscription, error) {
var subscriptions []subscription.Subscription
var channels = []string{chanOrderbook, chanPublicTrade, chanPublicTicker}
pairs, err := by.GetEnabledPairs(asset.Options)
if err != nil {
@@ -48,10 +49,10 @@ func (by *Bybit) GenerateOptionsDefaultSubscriptions() ([]stream.ChannelSubscrip
for z := range pairs {
for x := range channels {
subscriptions = append(subscriptions,
stream.ChannelSubscription{
Channel: channels[x],
Currency: pairs[z],
Asset: asset.Options,
subscription.Subscription{
Channel: channels[x],
Pair: pairs[z],
Asset: asset.Options,
})
}
}
@@ -59,16 +60,16 @@ func (by *Bybit) GenerateOptionsDefaultSubscriptions() ([]stream.ChannelSubscrip
}
// OptionSubscribe sends a subscription message to options public channels.
func (by *Bybit) OptionSubscribe(channelSubscriptions []stream.ChannelSubscription) error {
func (by *Bybit) OptionSubscribe(channelSubscriptions []subscription.Subscription) error {
return by.handleOptionsPayloadSubscription("subscribe", channelSubscriptions)
}
// OptionUnsubscribe sends an unsubscription messages through options public channels.
func (by *Bybit) OptionUnsubscribe(channelSubscriptions []stream.ChannelSubscription) error {
func (by *Bybit) OptionUnsubscribe(channelSubscriptions []subscription.Subscription) error {
return by.handleOptionsPayloadSubscription("unsubscribe", channelSubscriptions)
}
func (by *Bybit) handleOptionsPayloadSubscription(operation string, channelSubscriptions []stream.ChannelSubscription) error {
func (by *Bybit) handleOptionsPayloadSubscription(operation string, channelSubscriptions []subscription.Subscription) error {
payloads, err := by.handleSubscriptions(asset.Options, operation, channelSubscriptions)
if err != nil {
return err

View File

@@ -19,6 +19,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"
)
@@ -133,11 +134,11 @@ func (by *Bybit) WsAuth(ctx context.Context) error {
}
// Subscribe sends a websocket message to receive data from the channel
func (by *Bybit) Subscribe(channelsToSubscribe []stream.ChannelSubscription) error {
func (by *Bybit) Subscribe(channelsToSubscribe []subscription.Subscription) error {
return by.handleSpotSubscription("subscribe", channelsToSubscribe)
}
func (by *Bybit) handleSubscriptions(assetType asset.Item, operation string, channelsToSubscribe []stream.ChannelSubscription) ([]SubscriptionArgument, error) {
func (by *Bybit) handleSubscriptions(assetType asset.Item, operation string, channelsToSubscribe []subscription.Subscription) ([]SubscriptionArgument, error) {
var args []SubscriptionArgument
arg := SubscriptionArgument{
Operation: operation,
@@ -167,15 +168,15 @@ func (by *Bybit) handleSubscriptions(assetType asset.Item, operation string, cha
for i := range channelsToSubscribe {
switch channelsToSubscribe[i].Channel {
case chanOrderbook:
arg.Arguments = append(arg.Arguments, fmt.Sprintf("%s.%d.%s", channelsToSubscribe[i].Channel, 50, channelsToSubscribe[i].Currency.Format(pairFormat).String()))
arg.Arguments = append(arg.Arguments, fmt.Sprintf("%s.%d.%s", channelsToSubscribe[i].Channel, 50, channelsToSubscribe[i].Pair.Format(pairFormat).String()))
case chanPublicTrade, chanPublicTicker, chanLiquidation, chanLeverageTokenTicker, chanLeverageTokenNav:
arg.Arguments = append(arg.Arguments, channelsToSubscribe[i].Channel+"."+channelsToSubscribe[i].Currency.Format(pairFormat).String())
arg.Arguments = append(arg.Arguments, channelsToSubscribe[i].Channel+"."+channelsToSubscribe[i].Pair.Format(pairFormat).String())
case chanKline, chanLeverageTokenKline:
interval, err := intervalToString(kline.FiveMin)
if err != nil {
return nil, err
}
arg.Arguments = append(arg.Arguments, channelsToSubscribe[i].Channel+"."+interval+"."+channelsToSubscribe[i].Currency.Format(pairFormat).String())
arg.Arguments = append(arg.Arguments, channelsToSubscribe[i].Channel+"."+interval+"."+channelsToSubscribe[i].Pair.Format(pairFormat).String())
case chanPositions, chanExecution, chanOrder, chanWallet, chanGreeks, chanDCP:
if chanMap[channelsToSubscribe[i].Channel]&selectedChannels > 0 {
continue
@@ -203,11 +204,11 @@ func (by *Bybit) handleSubscriptions(assetType asset.Item, operation string, cha
}
// Unsubscribe sends a websocket message to stop receiving data from the channel
func (by *Bybit) Unsubscribe(channelsToUnsubscribe []stream.ChannelSubscription) error {
func (by *Bybit) Unsubscribe(channelsToUnsubscribe []subscription.Subscription) error {
return by.handleSpotSubscription("unsubscribe", channelsToUnsubscribe)
}
func (by *Bybit) handleSpotSubscription(operation string, channelsToSubscribe []stream.ChannelSubscription) error {
func (by *Bybit) handleSpotSubscription(operation string, channelsToSubscribe []subscription.Subscription) error {
payloads, err := by.handleSubscriptions(asset.Spot, operation, channelsToSubscribe)
if err != nil {
return err
@@ -238,8 +239,8 @@ func (by *Bybit) handleSpotSubscription(operation string, channelsToSubscribe []
}
// GenerateDefaultSubscriptions generates default subscription
func (by *Bybit) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, error) {
var subscriptions []stream.ChannelSubscription
func (by *Bybit) GenerateDefaultSubscriptions() ([]subscription.Subscription, error) {
var subscriptions []subscription.Subscription
var channels = []string{
chanPublicTicker,
chanOrderbook,
@@ -265,17 +266,17 @@ func (by *Bybit) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, e
chanDCP,
chanWallet:
subscriptions = append(subscriptions,
stream.ChannelSubscription{
subscription.Subscription{
Channel: channels[x],
Asset: asset.Spot,
})
default:
for z := range pairs {
subscriptions = append(subscriptions,
stream.ChannelSubscription{
Channel: channels[x],
Currency: pairs[z],
Asset: asset.Spot,
subscription.Subscription{
Channel: channels[x],
Pair: pairs[z],
Asset: asset.Spot,
})
}
}

View File

@@ -22,6 +22,7 @@ import (
"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/banking"
"github.com/thrasher-corp/gocryptotrader/portfolio/withdraw"
)
@@ -704,10 +705,10 @@ func TestWsAuth(t *testing.T) {
}
go c.wsReadData()
err = c.Subscribe([]stream.ChannelSubscription{
err = c.Subscribe([]subscription.Subscription{
{
Channel: "user",
Currency: testPair,
Channel: "user",
Pair: testPair,
},
})
if err != nil {

View File

@@ -19,6 +19,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"
)
@@ -365,32 +366,32 @@ func (c *CoinbasePro) ProcessUpdate(update *WebsocketL2Update) error {
}
// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions()
func (c *CoinbasePro) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, error) {
func (c *CoinbasePro) GenerateDefaultSubscriptions() ([]subscription.Subscription, error) {
var channels = []string{"heartbeat",
"level2_batch", /*Other orderbook feeds require authentication. This is batched in 50ms lots.*/
"ticker",
"user",
"matches"}
enabledCurrencies, err := c.GetEnabledPairs(asset.Spot)
enabledPairs, err := c.GetEnabledPairs(asset.Spot)
if err != nil {
return nil, err
}
var subscriptions []stream.ChannelSubscription
var subscriptions []subscription.Subscription
for i := range channels {
if (channels[i] == "user" || channels[i] == "full") &&
!c.IsWebsocketAuthenticationSupported() {
continue
}
for j := range enabledCurrencies {
fPair, err := c.FormatExchangeCurrency(enabledCurrencies[j],
for j := range enabledPairs {
fPair, err := c.FormatExchangeCurrency(enabledPairs[j],
asset.Spot)
if err != nil {
return nil, err
}
subscriptions = append(subscriptions, stream.ChannelSubscription{
Channel: channels[i],
Currency: fPair,
Asset: asset.Spot,
subscriptions = append(subscriptions, subscription.Subscription{
Channel: channels[i],
Pair: fPair,
Asset: asset.Spot,
})
}
}
@@ -398,7 +399,7 @@ func (c *CoinbasePro) GenerateDefaultSubscriptions() ([]stream.ChannelSubscripti
}
// Subscribe sends a websocket message to receive data from the channel
func (c *CoinbasePro) Subscribe(channelsToSubscribe []stream.ChannelSubscription) error {
func (c *CoinbasePro) Subscribe(channelsToSubscribe []subscription.Subscription) error {
var creds *account.Credentials
var err error
if c.IsWebsocketAuthenticationSupported() {
@@ -413,7 +414,7 @@ func (c *CoinbasePro) Subscribe(channelsToSubscribe []stream.ChannelSubscription
}
productIDs := make([]string, 0, len(channelsToSubscribe))
for i := range channelsToSubscribe {
p := channelsToSubscribe[i].Currency.String()
p := channelsToSubscribe[i].Pair.String()
if p != "" && !common.StringDataCompare(productIDs, p) {
// get all unique productIDs in advance as we generate by channels
productIDs = append(productIDs, p)
@@ -459,13 +460,13 @@ subscriptions:
}
// Unsubscribe sends a websocket message to stop receiving data from the channel
func (c *CoinbasePro) Unsubscribe(channelsToUnsubscribe []stream.ChannelSubscription) error {
func (c *CoinbasePro) Unsubscribe(channelsToUnsubscribe []subscription.Subscription) error {
unsubscribe := WebsocketSubscribe{
Type: "unsubscribe",
}
productIDs := make([]string, 0, len(channelsToUnsubscribe))
for i := range channelsToUnsubscribe {
p := channelsToUnsubscribe[i].Currency.String()
p := channelsToUnsubscribe[i].Pair.String()
if p != "" && !common.StringDataCompare(productIDs, p) {
// get all unique productIDs in advance as we generate by channels
productIDs = append(productIDs, p)

View File

@@ -18,6 +18,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"
@@ -597,19 +598,19 @@ func (c *COINUT) WsProcessOrderbookUpdate(update *WsOrderbookUpdate) error {
}
// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions()
func (c *COINUT) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, error) {
func (c *COINUT) GenerateDefaultSubscriptions() ([]subscription.Subscription, error) {
var channels = []string{"inst_tick", "inst_order_book", "inst_trade"}
var subscriptions []stream.ChannelSubscription
enabledCurrencies, err := c.GetEnabledPairs(asset.Spot)
var subscriptions []subscription.Subscription
enabledPairs, err := c.GetEnabledPairs(asset.Spot)
if err != nil {
return nil, err
}
for i := range channels {
for j := range enabledCurrencies {
subscriptions = append(subscriptions, stream.ChannelSubscription{
Channel: channels[i],
Currency: enabledCurrencies[j],
Asset: asset.Spot,
for j := range enabledPairs {
subscriptions = append(subscriptions, subscription.Subscription{
Channel: channels[i],
Pair: enabledPairs[j],
Asset: asset.Spot,
})
}
}
@@ -617,10 +618,10 @@ func (c *COINUT) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, e
}
// Subscribe sends a websocket message to receive data from the channel
func (c *COINUT) Subscribe(channelsToSubscribe []stream.ChannelSubscription) error {
func (c *COINUT) Subscribe(channelsToSubscribe []subscription.Subscription) error {
var errs error
for i := range channelsToSubscribe {
fPair, err := c.FormatExchangeCurrency(channelsToSubscribe[i].Currency, asset.Spot)
fPair, err := c.FormatExchangeCurrency(channelsToSubscribe[i].Pair, asset.Spot)
if err != nil {
errs = common.AppendError(errs, err)
continue
@@ -646,10 +647,10 @@ func (c *COINUT) Subscribe(channelsToSubscribe []stream.ChannelSubscription) err
}
// Unsubscribe sends a websocket message to stop receiving data from the channel
func (c *COINUT) Unsubscribe(channelToUnsubscribe []stream.ChannelSubscription) error {
func (c *COINUT) Unsubscribe(channelToUnsubscribe []subscription.Subscription) error {
var errs error
for i := range channelToUnsubscribe {
fPair, err := c.FormatExchangeCurrency(channelToUnsubscribe[i].Currency, asset.Spot)
fPair, err := c.FormatExchangeCurrency(channelToUnsubscribe[i].Pair, asset.Spot)
if err != nil {
errs = common.AppendError(errs, err)
continue

View File

@@ -9,6 +9,7 @@ import (
"sort"
"strconv"
"strings"
"sync"
"time"
"unicode"
@@ -28,6 +29,7 @@ import (
"github.com/thrasher-corp/gocryptotrader/exchanges/protocol"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"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"
@@ -61,6 +63,7 @@ var (
errAssetConfigFormatIsNil = errors.New("asset type config format is nil")
errSetDefaultsNotCalled = errors.New("set defaults not called")
errExchangeIsNil = errors.New("exchange is nil")
errBatchSizeZero = errors.New("batch size cannot be 0")
)
// SetRequester sets the instance of the requester
@@ -157,10 +160,38 @@ func (b *Base) SetFeatureDefaults() {
b.SetFillsFeedStatus(b.Config.Features.Enabled.FillsFeed)
}
b.SetSubscriptionsFromConfig()
b.Features.Enabled.AutoPairUpdates = b.Config.Features.Enabled.AutoPairUpdates
}
}
// SetSubscriptionsFromConfig sets the subscriptions from config
// If the subscriptions config is empty then Config will be updated from the exchange subscriptions,
// allowing e.SetDefaults to set default subscriptions for an exchange to update user's config
// Subscriptions not Enabled are skipped, meaning that e.Features.Subscriptions only contains Enabled subscriptions
func (b *Base) SetSubscriptionsFromConfig() {
b.settingsMutex.Lock()
defer b.settingsMutex.Unlock()
if len(b.Config.Features.Subscriptions) == 0 {
b.Config.Features.Subscriptions = b.Features.Subscriptions
return
}
b.Features.Subscriptions = []*subscription.Subscription{}
for _, s := range b.Config.Features.Subscriptions {
if s.Enabled {
b.Features.Subscriptions = append(b.Features.Subscriptions, s)
}
}
if b.Verbose {
names := make([]string, 0, len(b.Features.Subscriptions))
for _, s := range b.Features.Subscriptions {
names = append(names, s.Channel)
}
log.Debugf(log.ExchangeSys, "Set %v 'Subscriptions' to %v", b.Name, strings.Join(names, ", "))
}
}
// SupportsRESTTickerBatchUpdates returns whether or not the
// exchange supports REST batch ticker fetching
func (b *Base) SupportsRESTTickerBatchUpdates() bool {
@@ -1135,7 +1166,7 @@ func (b *Base) FlushWebsocketChannels() error {
// SubscribeToWebsocketChannels appends to ChannelsToSubscribe
// which lets websocket.manageSubscriptions handle subscribing
func (b *Base) SubscribeToWebsocketChannels(channels []stream.ChannelSubscription) error {
func (b *Base) SubscribeToWebsocketChannels(channels []subscription.Subscription) error {
if b.Websocket == nil {
return common.ErrFunctionNotSupported
}
@@ -1144,7 +1175,7 @@ func (b *Base) SubscribeToWebsocketChannels(channels []stream.ChannelSubscriptio
// UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe
// which lets websocket.manageSubscriptions handle unsubscribing
func (b *Base) UnsubscribeToWebsocketChannels(channels []stream.ChannelSubscription) error {
func (b *Base) UnsubscribeToWebsocketChannels(channels []subscription.Subscription) error {
if b.Websocket == nil {
return common.ErrFunctionNotSupported
}
@@ -1152,7 +1183,7 @@ func (b *Base) UnsubscribeToWebsocketChannels(channels []stream.ChannelSubscript
}
// GetSubscriptions returns a copied list of subscriptions
func (b *Base) GetSubscriptions() ([]stream.ChannelSubscription, error) {
func (b *Base) GetSubscriptions() ([]subscription.Subscription, error) {
if b.Websocket == nil {
return nil, common.ErrFunctionNotSupported
}
@@ -1811,3 +1842,37 @@ func (b *Base) IsPairEnabled(pair currency.Pair, a asset.Item) (bool, error) {
func (b *Base) GetOpenInterest(context.Context, ...key.PairAsset) ([]futures.OpenInterest, error) {
return nil, common.ErrFunctionNotSupported
}
// ParallelChanOp performs a single method call in parallel across streams and waits to return any errors
func (b *Base) ParallelChanOp(channels []subscription.Subscription, m func([]subscription.Subscription) error, batchSize int) error {
wg := sync.WaitGroup{}
errC := make(chan error, len(channels))
if batchSize == 0 {
return errBatchSizeZero
}
var j int
for i := 0; i < len(channels); i += batchSize {
j += batchSize
if j >= len(channels) {
j = len(channels)
}
wg.Add(1)
go func(c []subscription.Subscription) {
defer wg.Done()
if err := m(c); err != nil {
errC <- err
}
}(channels[i:j])
}
wg.Wait()
close(errC)
var errs error
for err := range errC {
errs = common.AppendError(errs, err)
}
return errs
}

View File

@@ -22,6 +22,7 @@ import (
"github.com/thrasher-corp/gocryptotrader/exchanges/protocol"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"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/portfolio/banking"
)
@@ -1262,8 +1263,8 @@ func TestSetupDefaults(t *testing.T) {
DefaultURL: "ws://something.com",
RunningURL: "ws://something.com",
Connector: func() error { return nil },
GenerateSubscriptions: func() ([]stream.ChannelSubscription, error) { return []stream.ChannelSubscription{}, nil },
Subscriber: func(cs []stream.ChannelSubscription) error { return nil },
GenerateSubscriptions: func() ([]subscription.Subscription, error) { return []subscription.Subscription{}, nil },
Subscriber: func(cs []subscription.Subscription) error { return nil },
})
if err != nil {
t.Fatal(err)
@@ -1611,8 +1612,8 @@ func TestIsWebsocketEnabled(t *testing.T) {
DefaultURL: "ws://something.com",
RunningURL: "ws://something.com",
Connector: func() error { return nil },
GenerateSubscriptions: func() ([]stream.ChannelSubscription, error) { return nil, nil },
Subscriber: func(cs []stream.ChannelSubscription) error { return nil },
GenerateSubscriptions: func() ([]subscription.Subscription, error) { return nil, nil },
Subscriber: func(cs []subscription.Subscription) error { return nil },
})
if err != nil {
t.Error(err)
@@ -3269,3 +3270,63 @@ func TestGetCachedOpenInterest(t *testing.T) {
})
assert.NoError(t, err)
}
// TestSetSubscriptionsFromConfig tests the setting and loading of subscriptions from config and exchange defaults
func TestSetSubscriptionsFromConfig(t *testing.T) {
t.Parallel()
b := Base{
Config: &config.Exchange{
Features: &config.FeaturesConfig{},
},
}
subs := []*subscription.Subscription{
{Channel: subscription.CandlesChannel, Interval: kline.OneDay, Enabled: true},
}
b.Features.Subscriptions = subs
b.SetSubscriptionsFromConfig()
assert.ElementsMatch(t, subs, b.Config.Features.Subscriptions, "Config Subscriptions should be updated")
assert.ElementsMatch(t, subs, b.Features.Subscriptions, "Subscriptions should be the same")
subs = []*subscription.Subscription{
{Channel: subscription.OrderbookChannel, Interval: kline.OneDay, Enabled: true},
}
b.Config.Features.Subscriptions = subs
b.SetSubscriptionsFromConfig()
assert.ElementsMatch(t, subs, b.Features.Subscriptions, "Subscriptions should be updated from Config")
assert.ElementsMatch(t, subs, b.Config.Features.Subscriptions, "Config Subscriptions should be the same")
}
// TestParallelChanOp unit tests the helper func ParallelChanOp
func TestParallelChanOp(t *testing.T) {
t.Parallel()
c := []subscription.Subscription{
{Channel: "red"},
{Channel: "blue"},
{Channel: "violent"},
{Channel: "spin"},
{Channel: "charm"},
}
run := make(chan struct{}, len(c)*2)
b := Base{}
errC := make(chan error, 1)
go func() {
errC <- b.ParallelChanOp(c, func(c []subscription.Subscription) error {
time.Sleep(300 * time.Millisecond)
run <- struct{}{}
switch c[0].Channel {
case "spin", "violent":
return errors.New(c[0].Channel)
}
return nil
}, 1)
}()
f := func(ct *assert.CollectT) {
if assert.Len(ct, errC, 1, "Should eventually have an error") {
err := <-errC
assert.ErrorContains(ct, err, "violent", "Should get a violent error")
assert.ErrorContains(ct, err, "spin", "Should get a spin error")
}
}
assert.EventuallyWithT(t, f, 500*time.Millisecond, 50*time.Millisecond, "ParallelChanOp should complete within 500ms not 5*300ms")
assert.Len(t, run, len(c), "Every channel was run to completion")
}

View File

@@ -14,6 +14,7 @@ import (
"github.com/thrasher-corp/gocryptotrader/exchanges/protocol"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"github.com/thrasher-corp/gocryptotrader/exchanges/stream"
"github.com/thrasher-corp/gocryptotrader/exchanges/subscription"
)
// Endpoint authentication types
@@ -149,8 +150,9 @@ type WithdrawalHistory struct {
// Features stores the supported and enabled features
// for the exchange
type Features struct {
Supports FeaturesSupported
Enabled FeaturesEnabled
Supports FeaturesSupported
Enabled FeaturesEnabled
Subscriptions []*subscription.Subscription
}
// FeaturesEnabled stores the exchange enabled features

View File

@@ -23,6 +23,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"
)
@@ -624,7 +625,7 @@ func (g *Gateio) processCrossMarginLoans(data []byte) error {
}
// GenerateDefaultSubscriptions returns default subscriptions
func (g *Gateio) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, error) {
func (g *Gateio) GenerateDefaultSubscriptions() ([]subscription.Subscription, error) {
channelsToSubscribe := defaultSubscriptions
if g.Websocket.CanUseAuthenticatedEndpoints() {
channelsToSubscribe = append(channelsToSubscribe, []string{
@@ -637,7 +638,7 @@ func (g *Gateio) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, e
channelsToSubscribe = append(channelsToSubscribe, spotTradesChannel)
}
var subscriptions []stream.ChannelSubscription
var subscriptions []subscription.Subscription
var err error
for i := range channelsToSubscribe {
var pairs []currency.Pair
@@ -677,11 +678,11 @@ func (g *Gateio) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, e
return nil, err
}
subscriptions = append(subscriptions, stream.ChannelSubscription{
Channel: channelsToSubscribe[i],
Currency: fpair.Upper(),
Asset: assetType,
Params: params,
subscriptions = append(subscriptions, subscription.Subscription{
Channel: channelsToSubscribe[i],
Pair: fpair.Upper(),
Asset: assetType,
Params: params,
})
}
}
@@ -689,7 +690,7 @@ func (g *Gateio) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, e
}
// handleSubscription sends a websocket message to receive data from the channel
func (g *Gateio) handleSubscription(event string, channelsToSubscribe []stream.ChannelSubscription) error {
func (g *Gateio) handleSubscription(event string, channelsToSubscribe []subscription.Subscription) error {
payloads, err := g.generatePayload(event, channelsToSubscribe)
if err != nil {
return err
@@ -719,7 +720,7 @@ func (g *Gateio) handleSubscription(event string, channelsToSubscribe []stream.C
return errs
}
func (g *Gateio) generatePayload(event string, channelsToSubscribe []stream.ChannelSubscription) ([]WsInput, error) {
func (g *Gateio) generatePayload(event string, channelsToSubscribe []subscription.Subscription) ([]WsInput, error) {
if len(channelsToSubscribe) == 0 {
return nil, errors.New("cannot generate payload, no channels supplied")
}
@@ -737,8 +738,8 @@ func (g *Gateio) generatePayload(event string, channelsToSubscribe []stream.Chan
for i := range channelsToSubscribe {
var auth *WsAuthInput
timestamp := time.Now()
channelsToSubscribe[i].Currency.Delimiter = currency.UnderscoreDelimiter
params := []string{channelsToSubscribe[i].Currency.String()}
channelsToSubscribe[i].Pair.Delimiter = currency.UnderscoreDelimiter
params := []string{channelsToSubscribe[i].Pair.String()}
switch channelsToSubscribe[i].Channel {
case spotOrderbookChannel:
interval, okay := channelsToSubscribe[i].Params["interval"].(kline.Interval)
@@ -836,12 +837,12 @@ func (g *Gateio) generatePayload(event string, channelsToSubscribe []stream.Chan
}
// Subscribe sends a websocket message to stop receiving data from the channel
func (g *Gateio) Subscribe(channelsToUnsubscribe []stream.ChannelSubscription) error {
func (g *Gateio) Subscribe(channelsToUnsubscribe []subscription.Subscription) error {
return g.handleSubscription("subscribe", channelsToUnsubscribe)
}
// Unsubscribe sends a websocket message to stop receiving data from the channel
func (g *Gateio) Unsubscribe(channelsToUnsubscribe []stream.ChannelSubscription) error {
func (g *Gateio) Unsubscribe(channelsToUnsubscribe []subscription.Subscription) error {
return g.handleSubscription("unsubscribe", channelsToUnsubscribe)
}

View File

@@ -16,6 +16,7 @@ import (
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/stream"
"github.com/thrasher-corp/gocryptotrader/exchanges/subscription"
"github.com/thrasher-corp/gocryptotrader/log"
)
@@ -140,7 +141,7 @@ func (g *Gateio) wsFunnelDeliveryFuturesConnectionData(ws stream.Connection) {
}
// GenerateDeliveryFuturesDefaultSubscriptions returns delivery futures default subscriptions params.
func (g *Gateio) GenerateDeliveryFuturesDefaultSubscriptions() ([]stream.ChannelSubscription, error) {
func (g *Gateio) GenerateDeliveryFuturesDefaultSubscriptions() ([]subscription.Subscription, error) {
_, err := g.GetCredentials(context.Background())
if err != nil {
g.Websocket.SetCanUseAuthenticatedEndpoints(false)
@@ -158,7 +159,7 @@ func (g *Gateio) GenerateDeliveryFuturesDefaultSubscriptions() ([]stream.Channel
if err != nil {
return nil, err
}
var subscriptions []stream.ChannelSubscription
var subscriptions []subscription.Subscription
for i := range channelsToSubscribe {
for j := range pairs {
params := make(map[string]interface{})
@@ -173,10 +174,10 @@ func (g *Gateio) GenerateDeliveryFuturesDefaultSubscriptions() ([]stream.Channel
if err != nil {
return nil, err
}
subscriptions = append(subscriptions, stream.ChannelSubscription{
Channel: channelsToSubscribe[i],
Currency: fpair.Upper(),
Params: params,
subscriptions = append(subscriptions, subscription.Subscription{
Channel: channelsToSubscribe[i],
Pair: fpair.Upper(),
Params: params,
})
}
}
@@ -184,17 +185,17 @@ func (g *Gateio) GenerateDeliveryFuturesDefaultSubscriptions() ([]stream.Channel
}
// DeliveryFuturesSubscribe sends a websocket message to stop receiving data from the channel
func (g *Gateio) DeliveryFuturesSubscribe(channelsToUnsubscribe []stream.ChannelSubscription) error {
func (g *Gateio) DeliveryFuturesSubscribe(channelsToUnsubscribe []subscription.Subscription) error {
return g.handleDeliveryFuturesSubscription("subscribe", channelsToUnsubscribe)
}
// DeliveryFuturesUnsubscribe sends a websocket message to stop receiving data from the channel
func (g *Gateio) DeliveryFuturesUnsubscribe(channelsToUnsubscribe []stream.ChannelSubscription) error {
func (g *Gateio) DeliveryFuturesUnsubscribe(channelsToUnsubscribe []subscription.Subscription) error {
return g.handleDeliveryFuturesSubscription("unsubscribe", channelsToUnsubscribe)
}
// handleDeliveryFuturesSubscription sends a websocket message to receive data from the channel
func (g *Gateio) handleDeliveryFuturesSubscription(event string, channelsToSubscribe []stream.ChannelSubscription) error {
func (g *Gateio) handleDeliveryFuturesSubscription(event string, channelsToSubscribe []subscription.Subscription) error {
payloads, err := g.generateDeliveryFuturesPayload(event, channelsToSubscribe)
if err != nil {
return err
@@ -228,7 +229,7 @@ func (g *Gateio) handleDeliveryFuturesSubscription(event string, channelsToSubsc
return errs
}
func (g *Gateio) generateDeliveryFuturesPayload(event string, channelsToSubscribe []stream.ChannelSubscription) ([2][]WsInput, error) {
func (g *Gateio) generateDeliveryFuturesPayload(event string, channelsToSubscribe []subscription.Subscription) ([2][]WsInput, error) {
if len(channelsToSubscribe) == 0 {
return [2][]WsInput{}, errors.New("cannot generate payload, no channels supplied")
}
@@ -245,7 +246,7 @@ func (g *Gateio) generateDeliveryFuturesPayload(event string, channelsToSubscrib
var auth *WsAuthInput
timestamp := time.Now()
var params []string
params = []string{channelsToSubscribe[i].Currency.String()}
params = []string{channelsToSubscribe[i].Pair.String()}
if g.Websocket.CanUseAuthenticatedEndpoints() {
switch channelsToSubscribe[i].Channel {
case futuresOrdersChannel, futuresUserTradesChannel,
@@ -309,7 +310,7 @@ func (g *Gateio) generateDeliveryFuturesPayload(event string, channelsToSubscrib
params = append(params, intervalString)
}
}
if strings.HasPrefix(channelsToSubscribe[i].Currency.Quote.Upper().String(), "USDT") {
if strings.HasPrefix(channelsToSubscribe[i].Pair.Quote.Upper().String(), "USDT") {
payloads[0] = append(payloads[0], WsInput{
ID: g.Websocket.Conn.GenerateMessageID(false),
Event: event,

View File

@@ -20,6 +20,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"
@@ -121,7 +122,7 @@ func (g *Gateio) WsFuturesConnect() error {
}
// GenerateFuturesDefaultSubscriptions returns default subscriptions information.
func (g *Gateio) GenerateFuturesDefaultSubscriptions() ([]stream.ChannelSubscription, error) {
func (g *Gateio) GenerateFuturesDefaultSubscriptions() ([]subscription.Subscription, error) {
channelsToSubscribe := defaultFuturesSubscriptions
if g.Websocket.CanUseAuthenticatedEndpoints() {
channelsToSubscribe = append(channelsToSubscribe,
@@ -134,7 +135,7 @@ func (g *Gateio) GenerateFuturesDefaultSubscriptions() ([]stream.ChannelSubscrip
if err != nil {
return nil, err
}
subscriptions := make([]stream.ChannelSubscription, len(channelsToSubscribe)*len(pairs))
subscriptions := make([]subscription.Subscription, len(channelsToSubscribe)*len(pairs))
count := 0
for i := range channelsToSubscribe {
for j := range pairs {
@@ -153,10 +154,10 @@ func (g *Gateio) GenerateFuturesDefaultSubscriptions() ([]stream.ChannelSubscrip
if err != nil {
return nil, err
}
subscriptions[count] = stream.ChannelSubscription{
Channel: channelsToSubscribe[i],
Currency: fpair.Upper(),
Params: params,
subscriptions[count] = subscription.Subscription{
Channel: channelsToSubscribe[i],
Pair: fpair.Upper(),
Params: params,
}
count++
}
@@ -165,12 +166,12 @@ func (g *Gateio) GenerateFuturesDefaultSubscriptions() ([]stream.ChannelSubscrip
}
// FuturesSubscribe sends a websocket message to stop receiving data from the channel
func (g *Gateio) FuturesSubscribe(channelsToUnsubscribe []stream.ChannelSubscription) error {
func (g *Gateio) FuturesSubscribe(channelsToUnsubscribe []subscription.Subscription) error {
return g.handleFuturesSubscription("subscribe", channelsToUnsubscribe)
}
// FuturesUnsubscribe sends a websocket message to stop receiving data from the channel
func (g *Gateio) FuturesUnsubscribe(channelsToUnsubscribe []stream.ChannelSubscription) error {
func (g *Gateio) FuturesUnsubscribe(channelsToUnsubscribe []subscription.Subscription) error {
return g.handleFuturesSubscription("unsubscribe", channelsToUnsubscribe)
}
@@ -269,7 +270,7 @@ func (g *Gateio) wsHandleFuturesData(respRaw []byte, assetType asset.Item) error
}
// handleFuturesSubscription sends a websocket message to receive data from the channel
func (g *Gateio) handleFuturesSubscription(event string, channelsToSubscribe []stream.ChannelSubscription) error {
func (g *Gateio) handleFuturesSubscription(event string, channelsToSubscribe []subscription.Subscription) error {
payloads, err := g.generateFuturesPayload(event, channelsToSubscribe)
if err != nil {
return err
@@ -306,7 +307,7 @@ func (g *Gateio) handleFuturesSubscription(event string, channelsToSubscribe []s
return nil
}
func (g *Gateio) generateFuturesPayload(event string, channelsToSubscribe []stream.ChannelSubscription) ([2][]WsInput, error) {
func (g *Gateio) generateFuturesPayload(event string, channelsToSubscribe []subscription.Subscription) ([2][]WsInput, error) {
if len(channelsToSubscribe) == 0 {
return [2][]WsInput{}, errors.New("cannot generate payload, no channels supplied")
}
@@ -323,7 +324,7 @@ func (g *Gateio) generateFuturesPayload(event string, channelsToSubscribe []stre
var auth *WsAuthInput
timestamp := time.Now()
var params []string
params = []string{channelsToSubscribe[i].Currency.String()}
params = []string{channelsToSubscribe[i].Pair.String()}
if g.Websocket.CanUseAuthenticatedEndpoints() {
switch channelsToSubscribe[i].Channel {
case futuresOrdersChannel, futuresUserTradesChannel,
@@ -387,7 +388,7 @@ func (g *Gateio) generateFuturesPayload(event string, channelsToSubscribe []stre
params = append(params, intervalString)
}
}
if strings.HasPrefix(channelsToSubscribe[i].Currency.Quote.Upper().String(), "USDT") {
if strings.HasPrefix(channelsToSubscribe[i].Pair.Quote.Upper().String(), "USDT") {
payloads[0] = append(payloads[0], WsInput{
ID: g.Websocket.Conn.GenerateMessageID(false),
Event: event,

View File

@@ -20,6 +20,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"
@@ -104,7 +105,7 @@ func (g *Gateio) WsOptionsConnect() error {
}
// GenerateOptionsDefaultSubscriptions generates list of channel subscriptions for options asset type.
func (g *Gateio) GenerateOptionsDefaultSubscriptions() ([]stream.ChannelSubscription, error) {
func (g *Gateio) GenerateOptionsDefaultSubscriptions() ([]subscription.Subscription, error) {
channelsToSubscribe := defaultOptionsSubscriptions
var userID int64
if g.Websocket.CanUseAuthenticatedEndpoints() {
@@ -129,7 +130,7 @@ func (g *Gateio) GenerateOptionsDefaultSubscriptions() ([]stream.ChannelSubscrip
}
}
getEnabledPairs:
var subscriptions []stream.ChannelSubscription
var subscriptions []subscription.Subscription
pairs, err := g.GetEnabledPairs(asset.Options)
if err != nil {
return nil, err
@@ -162,17 +163,17 @@ getEnabledPairs:
if err != nil {
return nil, err
}
subscriptions = append(subscriptions, stream.ChannelSubscription{
Channel: channelsToSubscribe[i],
Currency: fpair.Upper(),
Params: params,
subscriptions = append(subscriptions, subscription.Subscription{
Channel: channelsToSubscribe[i],
Pair: fpair.Upper(),
Params: params,
})
}
}
return subscriptions, nil
}
func (g *Gateio) generateOptionsPayload(event string, channelsToSubscribe []stream.ChannelSubscription) ([]WsInput, error) {
func (g *Gateio) generateOptionsPayload(event string, channelsToSubscribe []subscription.Subscription) ([]WsInput, error) {
if len(channelsToSubscribe) == 0 {
return nil, errors.New("cannot generate payload, no channels supplied")
}
@@ -189,7 +190,7 @@ func (g *Gateio) generateOptionsPayload(event string, channelsToSubscribe []stre
optionsUnderlyingPriceChannel,
optionsUnderlyingCandlesticksChannel:
var uly currency.Pair
uly, err = g.GetUnderlyingFromCurrencyPair(channelsToSubscribe[i].Currency)
uly, err = g.GetUnderlyingFromCurrencyPair(channelsToSubscribe[i].Pair)
if err != nil {
return nil, err
}
@@ -197,8 +198,8 @@ func (g *Gateio) generateOptionsPayload(event string, channelsToSubscribe []stre
case optionsBalancesChannel:
// options.balance channel does not require underlying or contract
default:
channelsToSubscribe[i].Currency.Delimiter = currency.UnderscoreDelimiter
params = append(params, channelsToSubscribe[i].Currency.String())
channelsToSubscribe[i].Pair.Delimiter = currency.UnderscoreDelimiter
params = append(params, channelsToSubscribe[i].Pair.String())
}
switch channelsToSubscribe[i].Channel {
case optionsOrderbookChannel:
@@ -298,17 +299,17 @@ func (g *Gateio) wsReadOptionsConnData() {
}
// OptionsSubscribe sends a websocket message to stop receiving data for asset type options
func (g *Gateio) OptionsSubscribe(channelsToUnsubscribe []stream.ChannelSubscription) error {
func (g *Gateio) OptionsSubscribe(channelsToUnsubscribe []subscription.Subscription) error {
return g.handleOptionsSubscription("subscribe", channelsToUnsubscribe)
}
// OptionsUnsubscribe sends a websocket message to stop receiving data for asset type options
func (g *Gateio) OptionsUnsubscribe(channelsToUnsubscribe []stream.ChannelSubscription) error {
func (g *Gateio) OptionsUnsubscribe(channelsToUnsubscribe []subscription.Subscription) error {
return g.handleOptionsSubscription("unsubscribe", channelsToUnsubscribe)
}
// handleOptionsSubscription sends a websocket message to receive data from the channel
func (g *Gateio) handleOptionsSubscription(event string, channelsToSubscribe []stream.ChannelSubscription) error {
func (g *Gateio) handleOptionsSubscription(event string, channelsToSubscribe []subscription.Subscription) error {
payloads, err := g.generateOptionsPayload(event, channelsToSubscribe)
if err != nil {
return err

View File

@@ -21,6 +21,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/trade"
"github.com/thrasher-corp/gocryptotrader/log"
)
@@ -62,7 +63,7 @@ func (g *Gemini) WsConnect() error {
}
// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions()
func (g *Gemini) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, error) {
func (g *Gemini) GenerateDefaultSubscriptions() ([]subscription.Subscription, error) {
// See gemini_types.go for more subscription/candle vars
var channels = []string{
marketDataLevel2,
@@ -74,13 +75,13 @@ func (g *Gemini) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, e
return nil, err
}
var subscriptions []stream.ChannelSubscription
var subscriptions []subscription.Subscription
for x := range channels {
for y := range pairs {
subscriptions = append(subscriptions, stream.ChannelSubscription{
Channel: channels[x],
Currency: pairs[y],
Asset: asset.Spot,
subscriptions = append(subscriptions, subscription.Subscription{
Channel: channels[x],
Pair: pairs[y],
Asset: asset.Spot,
})
}
}
@@ -88,7 +89,7 @@ func (g *Gemini) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, e
}
// Subscribe sends a websocket message to receive data from the channel
func (g *Gemini) Subscribe(channelsToSubscribe []stream.ChannelSubscription) error {
func (g *Gemini) Subscribe(channelsToSubscribe []subscription.Subscription) error {
channels := make([]string, 0, len(channelsToSubscribe))
for x := range channelsToSubscribe {
if common.StringDataCompareInsensitive(channels, channelsToSubscribe[x].Channel) {
@@ -99,10 +100,10 @@ func (g *Gemini) Subscribe(channelsToSubscribe []stream.ChannelSubscription) err
var pairs currency.Pairs
for x := range channelsToSubscribe {
if pairs.Contains(channelsToSubscribe[x].Currency, true) {
if pairs.Contains(channelsToSubscribe[x].Pair, true) {
continue
}
pairs = append(pairs, channelsToSubscribe[x].Currency)
pairs = append(pairs, channelsToSubscribe[x].Pair)
}
fmtPairs, err := g.FormatExchangeCurrencies(pairs, asset.Spot)
@@ -132,7 +133,7 @@ func (g *Gemini) Subscribe(channelsToSubscribe []stream.ChannelSubscription) err
}
// Unsubscribe sends a websocket message to stop receiving data from the channel
func (g *Gemini) Unsubscribe(channelsToUnsubscribe []stream.ChannelSubscription) error {
func (g *Gemini) Unsubscribe(channelsToUnsubscribe []subscription.Subscription) error {
channels := make([]string, 0, len(channelsToUnsubscribe))
for x := range channelsToUnsubscribe {
if common.StringDataCompareInsensitive(channels, channelsToUnsubscribe[x].Channel) {
@@ -143,10 +144,10 @@ func (g *Gemini) Unsubscribe(channelsToUnsubscribe []stream.ChannelSubscription)
var pairs currency.Pairs
for x := range channelsToUnsubscribe {
if pairs.Contains(channelsToUnsubscribe[x].Currency, true) {
if pairs.Contains(channelsToUnsubscribe[x].Pair, true) {
continue
}
pairs = append(pairs, channelsToUnsubscribe[x].Currency)
pairs = append(pairs, channelsToUnsubscribe[x].Pair)
}
fmtPairs, err := g.FormatExchangeCurrencies(pairs, asset.Spot)

View File

@@ -18,6 +18,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"
@@ -465,15 +466,15 @@ func (h *HitBTC) WsProcessOrderbookUpdate(update *WsOrderbook) error {
}
// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions()
func (h *HitBTC) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, error) {
func (h *HitBTC) GenerateDefaultSubscriptions() ([]subscription.Subscription, error) {
var channels = []string{"subscribeTicker",
"subscribeOrderbook",
"subscribeTrades",
"subscribeCandles"}
var subscriptions []stream.ChannelSubscription
var subscriptions []subscription.Subscription
if h.Websocket.CanUseAuthenticatedEndpoints() {
subscriptions = append(subscriptions, stream.ChannelSubscription{
subscriptions = append(subscriptions, subscription.Subscription{
Channel: "subscribeReports",
})
}
@@ -489,10 +490,10 @@ func (h *HitBTC) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, e
}
enabledCurrencies[j].Delimiter = ""
subscriptions = append(subscriptions, stream.ChannelSubscription{
Channel: channels[i],
Currency: fPair,
Asset: asset.Spot,
subscriptions = append(subscriptions, subscription.Subscription{
Channel: channels[i],
Pair: fPair,
Asset: asset.Spot,
})
}
}
@@ -500,7 +501,7 @@ func (h *HitBTC) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, e
}
// Subscribe sends a websocket message to receive data from the channel
func (h *HitBTC) Subscribe(channelsToSubscribe []stream.ChannelSubscription) error {
func (h *HitBTC) Subscribe(channelsToSubscribe []subscription.Subscription) error {
var errs error
for i := range channelsToSubscribe {
subscribe := WsRequest{
@@ -508,8 +509,8 @@ func (h *HitBTC) Subscribe(channelsToSubscribe []stream.ChannelSubscription) err
ID: h.Websocket.Conn.GenerateMessageID(false),
}
if channelsToSubscribe[i].Currency.String() != "" {
subscribe.Params.Symbol = channelsToSubscribe[i].Currency.String()
if channelsToSubscribe[i].Pair.String() != "" {
subscribe.Params.Symbol = channelsToSubscribe[i].Pair.String()
}
if strings.EqualFold(channelsToSubscribe[i].Channel, "subscribeTrades") {
subscribe.Params.Limit = 100
@@ -532,7 +533,7 @@ func (h *HitBTC) Subscribe(channelsToSubscribe []stream.ChannelSubscription) err
}
// Unsubscribe sends a websocket message to stop receiving data from the channel
func (h *HitBTC) Unsubscribe(channelsToUnsubscribe []stream.ChannelSubscription) error {
func (h *HitBTC) Unsubscribe(channelsToUnsubscribe []subscription.Subscription) error {
var errs error
for i := range channelsToUnsubscribe {
unsubscribeChannel := strings.Replace(channelsToUnsubscribe[i].Channel,
@@ -545,7 +546,7 @@ func (h *HitBTC) Unsubscribe(channelsToUnsubscribe []stream.ChannelSubscription)
Method: unsubscribeChannel,
}
unsubscribe.Params.Symbol = channelsToUnsubscribe[i].Currency.String()
unsubscribe.Params.Symbol = channelsToUnsubscribe[i].Pair.String()
if strings.EqualFold(unsubscribeChannel, "unsubscribeTrades") {
unsubscribe.Params.Limit = 100
} else if strings.EqualFold(unsubscribeChannel, "unsubscribeCandles") {

View File

@@ -20,6 +20,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"
@@ -514,15 +515,15 @@ func (h *HUOBI) WsProcessOrderbook(update *WsDepth, symbol string) error {
}
// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions()
func (h *HUOBI) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, error) {
func (h *HUOBI) GenerateDefaultSubscriptions() ([]subscription.Subscription, error) {
var channels = []string{wsMarketKline,
wsMarketDepth,
wsMarketTrade,
wsMarketTicker}
var subscriptions []stream.ChannelSubscription
var subscriptions []subscription.Subscription
if h.Websocket.CanUseAuthenticatedEndpoints() {
channels = append(channels, "orders.%v", "orders.%v.update")
subscriptions = append(subscriptions, stream.ChannelSubscription{
subscriptions = append(subscriptions, subscription.Subscription{
Channel: "accounts",
})
}
@@ -535,9 +536,9 @@ func (h *HUOBI) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, er
enabledCurrencies[j].Delimiter = ""
channel := fmt.Sprintf(channels[i],
enabledCurrencies[j].Lower().String())
subscriptions = append(subscriptions, stream.ChannelSubscription{
Channel: channel,
Currency: enabledCurrencies[j],
subscriptions = append(subscriptions, subscription.Subscription{
Channel: channel,
Pair: enabledCurrencies[j],
})
}
}
@@ -545,7 +546,7 @@ func (h *HUOBI) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, er
}
// Subscribe sends a websocket message to receive data from the channel
func (h *HUOBI) Subscribe(channelsToSubscribe []stream.ChannelSubscription) error {
func (h *HUOBI) Subscribe(channelsToSubscribe []subscription.Subscription) error {
var creds *account.Credentials
if h.Websocket.CanUseAuthenticatedEndpoints() {
var err error
@@ -585,7 +586,7 @@ func (h *HUOBI) Subscribe(channelsToSubscribe []stream.ChannelSubscription) erro
}
// Unsubscribe sends a websocket message to stop receiving data from the channel
func (h *HUOBI) Unsubscribe(channelsToUnsubscribe []stream.ChannelSubscription) error {
func (h *HUOBI) Unsubscribe(channelsToUnsubscribe []subscription.Subscription) error {
var creds *account.Credentials
if h.Websocket.CanUseAuthenticatedEndpoints() {
var err error

View File

@@ -20,6 +20,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/portfolio/withdraw"
@@ -71,9 +72,9 @@ type IBotExchange interface {
EnableRateLimiter() error
GetServerTime(ctx context.Context, ai asset.Item) (time.Time, error)
GetWebsocket() (*stream.Websocket, error)
SubscribeToWebsocketChannels(channels []stream.ChannelSubscription) error
UnsubscribeToWebsocketChannels(channels []stream.ChannelSubscription) error
GetSubscriptions() ([]stream.ChannelSubscription, error)
SubscribeToWebsocketChannels(channels []subscription.Subscription) error
UnsubscribeToWebsocketChannels(channels []subscription.Subscription) error
GetSubscriptions() ([]subscription.Subscription, error)
FlushWebsocketChannels() error
AuthenticateWebsocket(ctx context.Context) error
GetOrderExecutionLimits(a asset.Item, cp currency.Pair) (order.MinMaxLevel, error)

View File

@@ -1,9 +1,11 @@
package kline
import (
"bytes"
"errors"
"fmt"
"sort"
"strconv"
"strings"
"time"
@@ -143,6 +145,31 @@ func (i Interval) Short() string {
return s
}
// UnmarshalJSON implements the json.Unmarshaler interface for Intervals
// It does not validate the duration is aligned, only that it is a parsable duration
func (i *Interval) UnmarshalJSON(text []byte) error {
text = bytes.Trim(text, `"`)
if len(bytes.TrimLeft(text, `0123456789`)) > 0 { // contains non-numerics, ParseDuration can handle errors
d, err := time.ParseDuration(string(text))
if err != nil {
return err
}
*i = Interval(d)
} else {
n, err := strconv.ParseInt(string(text), 10, 64)
if err != nil {
return err
}
*i = Interval(n)
}
return nil
}
// MarshalText implements the TextMarshaler interface for Intervals
func (i Interval) MarshalText() ([]byte, error) {
return []byte(i.Short()), nil
}
// addPadding inserts padding time aligned when exchanges do not supply all data
// when there is no activity in a certain time interval.
// Start defines the request start and due to potential no activity from this

View File

@@ -10,6 +10,7 @@ import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/database"
@@ -1396,3 +1397,18 @@ func TestGetIntervalResultLimit(t *testing.T) {
t.Errorf("received '%v' expected '%v'", limit, 1337)
}
}
func TestUnmarshalJSON(t *testing.T) {
i := new(Interval)
err := i.UnmarshalJSON([]byte(`"3m"`))
assert.NoError(t, err, "UnmarshalJSON should not error")
assert.Equal(t, time.Minute*3, i.Duration(), "Interval should have correct value")
err = i.UnmarshalJSON([]byte(`"15s"`))
assert.NoError(t, err, "UnmarshalJSON should not error")
assert.Equal(t, time.Second*15, i.Duration(), "Interval should have correct value")
err = i.UnmarshalJSON([]byte(`720000000000`))
assert.NoError(t, err, "UnmarshalJSON should not error")
assert.Equal(t, time.Minute*12, i.Duration(), "Interval should have correct value")
err = i.UnmarshalJSON([]byte(`"6hedgehogs"`))
assert.ErrorContains(t, err, "unknown unit", "UnmarshalJSON should error")
}

View File

@@ -29,6 +29,7 @@ import (
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
"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"
)
@@ -1245,10 +1246,10 @@ func setupWsTests(t *testing.T) {
// TestWebsocketSubscribe tests returning a message with an id
func TestWebsocketSubscribe(t *testing.T) {
setupWsTests(t)
err := k.Subscribe([]stream.ChannelSubscription{
err := k.Subscribe([]subscription.Subscription{
{
Channel: defaultSubscribedChannels[0],
Currency: currency.NewPairWithDelimiter("XBT", "USD", "/"),
Channel: defaultSubscribedChannels[0],
Pair: currency.NewPairWithDelimiter("XBT", "USD", "/"),
},
})
if err != nil {

View File

@@ -5,7 +5,7 @@ import (
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/stream"
"github.com/thrasher-corp/gocryptotrader/exchanges/subscription"
)
const (
@@ -497,11 +497,11 @@ type WithdrawStatusResponse struct {
// WebsocketSubscriptionEventRequest handles WS subscription events
type WebsocketSubscriptionEventRequest struct {
Event string `json:"event"` // subscribe
RequestID int64 `json:"reqid,omitempty"` // Optional, client originated ID reflected in response message.
Pairs []string `json:"pair,omitempty"` // Array of currency pairs (pair1,pair2,pair3).
Subscription WebsocketSubscriptionData `json:"subscription,omitempty"`
Channels []stream.ChannelSubscription `json:"-"` // Keeps track of associated subscriptions in batched outgoings
Event string `json:"event"` // subscribe
RequestID int64 `json:"reqid,omitempty"` // Optional, client originated ID reflected in response message.
Pairs []string `json:"pair,omitempty"` // Array of currency pairs (pair1,pair2,pair3).
Subscription WebsocketSubscriptionData `json:"subscription,omitempty"`
Channels []subscription.Subscription `json:"-"` // Keeps track of associated subscriptions in batched outgoings
}
// WebsocketBaseEventRequest Just has an "event" property

View File

@@ -20,6 +20,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"
@@ -846,7 +847,7 @@ func (k *Kraken) wsProcessOrderBook(channelData *WebsocketChannelData, data map[
if err != nil {
outbound := channelData.Pair // Format required "XBT/USD"
outbound.Delimiter = "/"
go func(resub *stream.ChannelSubscription) {
go func(resub *subscription.Subscription) {
// This was locking the main websocket reader routine and a
// backlog occurred. So put this into it's own go routine.
errResub := k.Websocket.ResubscribeToChannel(resub)
@@ -856,10 +857,10 @@ func (k *Kraken) wsProcessOrderBook(channelData *WebsocketChannelData, data map[
resub,
errResub)
}
}(&stream.ChannelSubscription{
Channel: krakenWsOrderbook,
Currency: outbound,
Asset: asset.Spot,
}(&subscription.Subscription{
Channel: krakenWsOrderbook,
Pair: outbound,
Asset: asset.Spot,
})
return err
}
@@ -1209,25 +1210,25 @@ func (k *Kraken) wsProcessCandles(channelData *WebsocketChannelData, data []inte
}
// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions()
func (k *Kraken) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, error) {
enabledCurrencies, err := k.GetEnabledPairs(asset.Spot)
func (k *Kraken) GenerateDefaultSubscriptions() ([]subscription.Subscription, error) {
enabledPairs, err := k.GetEnabledPairs(asset.Spot)
if err != nil {
return nil, err
}
var subscriptions []stream.ChannelSubscription
var subscriptions []subscription.Subscription
for i := range defaultSubscribedChannels {
for j := range enabledCurrencies {
enabledCurrencies[j].Delimiter = "/"
subscriptions = append(subscriptions, stream.ChannelSubscription{
Channel: defaultSubscribedChannels[i],
Currency: enabledCurrencies[j],
Asset: asset.Spot,
for j := range enabledPairs {
enabledPairs[j].Delimiter = "/"
subscriptions = append(subscriptions, subscription.Subscription{
Channel: defaultSubscribedChannels[i],
Pair: enabledPairs[j],
Asset: asset.Spot,
})
}
}
if k.Websocket.CanUseAuthenticatedEndpoints() {
for i := range authenticatedChannels {
subscriptions = append(subscriptions, stream.ChannelSubscription{
subscriptions = append(subscriptions, subscription.Subscription{
Channel: authenticatedChannels[i],
})
}
@@ -1236,7 +1237,7 @@ func (k *Kraken) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, e
}
// Subscribe sends a websocket message to receive data from the channel
func (k *Kraken) Subscribe(channelsToSubscribe []stream.ChannelSubscription) error {
func (k *Kraken) Subscribe(channelsToSubscribe []subscription.Subscription) error {
var subscriptions = make(map[string]*[]WebsocketSubscriptionEventRequest)
channels:
for i := range channelsToSubscribe {
@@ -1247,7 +1248,7 @@ channels:
}
for j := range *s {
(*s)[j].Pairs = append((*s)[j].Pairs, channelsToSubscribe[i].Currency.String())
(*s)[j].Pairs = append((*s)[j].Pairs, channelsToSubscribe[i].Pair.String())
(*s)[j].Channels = append((*s)[j].Channels, channelsToSubscribe[i])
continue channels
}
@@ -1263,8 +1264,8 @@ channels:
if channelsToSubscribe[i].Channel == "book" {
outbound.Subscription.Depth = krakenWsOrderbookDepth
}
if !channelsToSubscribe[i].Currency.IsEmpty() {
outbound.Pairs = []string{channelsToSubscribe[i].Currency.String()}
if !channelsToSubscribe[i].Pair.IsEmpty() {
outbound.Pairs = []string{channelsToSubscribe[i].Pair.String()}
}
if common.StringDataContains(authenticatedChannels, channelsToSubscribe[i].Channel) {
outbound.Subscription.Token = authToken
@@ -1298,14 +1299,14 @@ channels:
}
// Unsubscribe sends a websocket message to stop receiving data from the channel
func (k *Kraken) Unsubscribe(channelsToUnsubscribe []stream.ChannelSubscription) error {
func (k *Kraken) Unsubscribe(channelsToUnsubscribe []subscription.Subscription) error {
var unsubs []WebsocketSubscriptionEventRequest
channels:
for x := range channelsToUnsubscribe {
for y := range unsubs {
if unsubs[y].Subscription.Name == channelsToUnsubscribe[x].Channel {
unsubs[y].Pairs = append(unsubs[y].Pairs,
channelsToUnsubscribe[x].Currency.String())
channelsToUnsubscribe[x].Pair.String())
unsubs[y].Channels = append(unsubs[y].Channels,
channelsToUnsubscribe[x])
continue channels
@@ -1325,7 +1326,7 @@ channels:
unsub := WebsocketSubscriptionEventRequest{
Event: krakenWsUnsubscribe,
Pairs: []string{channelsToUnsubscribe[x].Currency.String()},
Pairs: []string{channelsToUnsubscribe[x].Pair.String()},
Subscription: WebsocketSubscriptionData{
Name: channelsToUnsubscribe[x].Channel,
Depth: depth,

View File

@@ -6,11 +6,13 @@ import (
"errors"
"log"
"os"
"strings"
"testing"
"time"
"github.com/gofrs/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/common/key"
"github.com/thrasher-corp/gocryptotrader/config"
@@ -24,9 +26,10 @@ 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/stream/buffer"
"github.com/thrasher-corp/gocryptotrader/exchanges/subscription"
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
testexch "github.com/thrasher-corp/gocryptotrader/internal/testing/exchange"
"github.com/thrasher-corp/gocryptotrader/portfolio/withdraw"
)
@@ -1974,10 +1977,133 @@ func TestPushData(t *testing.T) {
sharedtestvalues.TestFixtureToDataHandler(t, ku, n, "testdata/wsHandleData.json", ku.wsHandleData)
}
func verifySubs(tb testing.TB, subs []subscription.Subscription, a asset.Item, prefix string, expected ...string) {
tb.Helper()
var sub *subscription.Subscription
for i, s := range subs {
if s.Asset == a && strings.HasPrefix(s.Channel, prefix) {
if len(expected) == 1 && !strings.Contains(s.Channel, expected[0]) {
continue
}
if sub != nil {
assert.Failf(tb, "Too many subs with prefix", "Asset %s; Prefix %s", a.String(), prefix)
return
}
sub = &subs[i]
}
}
if assert.NotNil(tb, sub, "Should find a sub for asset %s with prefix %s for %s", a.String(), prefix, strings.Join(expected, ", ")) {
suffix := strings.TrimPrefix(sub.Channel, prefix)
if len(expected) == 0 {
assert.Empty(tb, suffix, "Sub for asset %s with prefix %s should have no symbol suffix", a.String(), prefix)
} else {
currs := strings.Split(suffix, ",")
assert.ElementsMatch(tb, currs, expected, "Currencies should match in sub for asset %s with prefix %s", a.String(), prefix)
}
}
}
// Pairs for Subscription tests:
// Only in Spot: BTC-USDT, ETH-USDT
// In Both: ETH-BTC, LTC-USDT
// Only in Margin: XMR-BTC, SOL-USDC
func TestGenerateDefaultSubscriptions(t *testing.T) {
t.Parallel()
if _, err := ku.GenerateDefaultSubscriptions(); err != nil {
t.Error(err)
subs, err := ku.GenerateDefaultSubscriptions()
assert.NoError(t, err, "GenerateDefaultSubscriptions should not error")
assert.Len(t, subs, 12, "Should generate the correct number of subs when not logged in")
for _, p := range []string{"ticker", "match", "level2"} {
verifySubs(t, subs, asset.Spot, "/market/"+p+":", "BTC-USDT", "ETH-USDT", "LTC-USDT", "ETH-BTC")
verifySubs(t, subs, asset.Margin, "/market/"+p+":", "SOL-USDC", "XMR-BTC")
}
for _, c := range []string{"ETHUSDCM", "XBTUSDCM", "SOLUSDTM"} {
verifySubs(t, subs, asset.Futures, "/contractMarket/tickerV2:", c)
verifySubs(t, subs, asset.Futures, "/contractMarket/level2Depth50:", c)
}
}
func TestGenerateAuthSubscriptions(t *testing.T) {
t.Parallel()
// Create a parallel safe Kucoin to mess with
nu := new(Kucoin)
nu.Base.Features = ku.Base.Features
assert.NoError(t, nu.CurrencyPairs.Load(&ku.CurrencyPairs), "Loading Pairs should not error")
nu.Websocket = sharedtestvalues.NewTestWebsocket()
nu.Websocket.SetCanUseAuthenticatedEndpoints(true)
subs, err := nu.GenerateDefaultSubscriptions()
assert.NoError(t, err, "GenerateDefaultSubscriptions with Auth should not error")
assert.Len(t, subs, 25, "Should generate the correct number of subs when logged in")
for _, p := range []string{"ticker", "match", "level2"} {
verifySubs(t, subs, asset.Spot, "/market/"+p+":", "BTC-USDT", "ETH-USDT", "LTC-USDT", "ETH-BTC")
verifySubs(t, subs, asset.Margin, "/market/"+p+":", "SOL-USDC", "XMR-BTC")
}
for _, c := range []string{"ETHUSDCM", "XBTUSDCM", "SOLUSDTM"} {
verifySubs(t, subs, asset.Futures, "/contractMarket/tickerV2:", c)
verifySubs(t, subs, asset.Futures, "/contractMarket/level2Depth50:", c)
}
for _, c := range []string{"SOL", "BTC", "XMR", "LTC", "USDC", "USDT", "ETH"} {
verifySubs(t, subs, asset.Margin, "/margin/loan:", c)
}
verifySubs(t, subs, asset.Spot, "/account/balance")
verifySubs(t, subs, asset.Margin, "/margin/position")
verifySubs(t, subs, asset.Margin, "/margin/fundingBook:", "SOL", "BTC", "XMR", "LTC", "USDT", "USDC", "ETH")
verifySubs(t, subs, asset.Futures, "/contractAccount/wallet")
verifySubs(t, subs, asset.Futures, "/contractMarket/advancedOrders")
verifySubs(t, subs, asset.Futures, "/contractMarket/tradeOrders")
}
func TestGenerateCandleSubscription(t *testing.T) {
t.Parallel()
// Create a parallel safe Kucoin to mess with
nu := new(Kucoin)
nu.Base.Features = ku.Base.Features
nu.Websocket = sharedtestvalues.NewTestWebsocket()
assert.NoError(t, nu.CurrencyPairs.Load(&ku.CurrencyPairs), "Loading Pairs should not error")
nu.Features.Subscriptions = []*subscription.Subscription{
{Channel: subscription.CandlesChannel, Interval: kline.FourHour},
}
subs, err := nu.GenerateDefaultSubscriptions()
assert.NoError(t, err, "GenerateDefaultSubscriptions with Candles should not error")
assert.Len(t, subs, 6, "Should generate the correct number of subs for candles")
for _, c := range []string{"BTC-USDT", "ETH-USDT", "LTC-USDT", "ETH-BTC"} {
verifySubs(t, subs, asset.Spot, "/market/candles:", c+"_4hour")
}
for _, c := range []string{"SOL-USDC", "XMR-BTC"} {
verifySubs(t, subs, asset.Margin, "/market/candles:", c+"_4hour")
}
}
func TestGenerateMarketSubscription(t *testing.T) {
t.Parallel()
// Create a parallel safe Kucoin to mess with
nu := new(Kucoin)
nu.Base.Features = ku.Base.Features
nu.Websocket = sharedtestvalues.NewTestWebsocket()
assert.NoError(t, nu.CurrencyPairs.Load(&ku.CurrencyPairs), "Loading Pairs should not error")
nu.Features.Subscriptions = []*subscription.Subscription{
{Channel: marketSnapshotChannel},
}
subs, err := nu.GenerateDefaultSubscriptions()
assert.NoError(t, err, "GenerateDefaultSubscriptions with MarketSnapshot should not error")
assert.Len(t, subs, 7, "Should generate the correct number of subs for snapshot")
for _, c := range []string{"BTC", "ETH", "LTC", "USDT"} {
verifySubs(t, subs, asset.Spot, "/market/snapshot:", c)
}
for _, c := range []string{"SOL", "USDC", "XMR"} {
verifySubs(t, subs, asset.Margin, "/market/snapshot:", c)
}
}
@@ -2155,21 +2281,6 @@ func TestCancelAllOrders(t *testing.T) {
}
}
func TestGeneratePayloads(t *testing.T) {
t.Parallel()
subscriptions, err := ku.GenerateDefaultSubscriptions()
if err != nil {
t.Error(err)
}
payload, err := ku.generatePayloads(subscriptions, "subscribe")
if err != nil {
t.Error(err)
}
if len(payload) != len(subscriptions) {
t.Error("derived payload is not same as generated channel subscription instances")
}
}
const (
subUserResponseJSON = `{"userId":"635002438793b80001dcc8b3", "uid":62356, "subName":"margin01", "status":2, "type":4, "access":"Margin", "createdAt":1666187844000, "remarks":null }`
positionSettlementPushData = `{"userId": "xbc453tg732eba53a88ggyt8c", "topic": "/contract/position:XBTUSDM", "subject": "position.settlement", "data": { "fundingTime": 1551770400000, "qty": 100, "markPrice": 3610.85, "fundingRate": -0.002966, "fundingFee": -296, "ts": 1547697294838004923, "settleCurrency": "XBT" } }`
@@ -2361,6 +2472,7 @@ func TestProcessMarketSnapshot(t *testing.T) {
n := new(Kucoin)
sharedtestvalues.TestFixtureToDataHandler(t, ku, n, "testdata/wsMarketSnapshot.json", n.wsHandleData)
seen := 0
seenAssetTypes := map[asset.Item]int{}
for reading := true; reading; {
select {
default:
@@ -2370,33 +2482,37 @@ func TestProcessMarketSnapshot(t *testing.T) {
switch v := resp.(type) {
case *ticker.Price:
switch seen {
// spot only
case 1:
assert.Equal(t, time.UnixMilli(1698740324415), v.LastUpdated, "datetime")
assert.Equal(t, 0.00001402100000000000, v.High, "high")
assert.Equal(t, 0.000012508, v.Last, "lastTradedPrice")
assert.Equal(t, 0.00001129200000000000, v.Low, "low")
assert.Equal(t, asset.Margin, v.AssetType, "AssetType")
assert.Equal(t, time.UnixMilli(1700555342007), v.LastUpdated, "datetime")
assert.Equal(t, 0.004445, v.High, "high")
assert.Equal(t, 0.004415, v.Last, "lastTradedPrice")
assert.Equal(t, 0.004191, v.Low, "low")
assert.Equal(t, currency.NewPairWithDelimiter("XMR", "BTC", "-"), v.Pair, "symbol")
assert.Equal(t, 28474.47280000000000000000, v.Volume, "volume")
assert.Equal(t, 0.37038038297340000000, v.QuoteVolume, "volValue")
// margin only
case 2:
assert.Equal(t, time.UnixMilli(1698740324483), v.LastUpdated, "datetime")
assert.Equal(t, 0.00000039450000000000, v.High, "high")
assert.Equal(t, 0.0000003897, v.Last, "lastTradedPrice")
assert.Equal(t, 0.00000034200000000000, v.Low, "low")
assert.Equal(t, 13097.3357, v.Volume, "volume")
assert.Equal(t, 57.44552981, v.QuoteVolume, "volValue")
case 2, 3:
assert.Equal(t, time.UnixMilli(1700555340197), v.LastUpdated, "datetime")
assert.Contains(t, []asset.Item{asset.Spot, asset.Margin}, v.AssetType, "AssetType is Spot or Margin")
seenAssetTypes[v.AssetType]++
assert.Equal(t, seenAssetTypes[v.AssetType], 1, "Each Asset Type is sent only once per unique snapshot")
assert.Equal(t, 0.054846, v.High, "high")
assert.Equal(t, 0.053778, v.Last, "lastTradedPrice")
assert.Equal(t, 0.05364, v.Low, "low")
assert.Equal(t, currency.NewPairWithDelimiter("ETH", "BTC", "-"), v.Pair, "symbol")
assert.Equal(t, 316078.69700000000000000000, v.Volume, "volume")
assert.Equal(t, 0.11768519138877000000, v.QuoteVolume, "volValue")
// both margin and spot
case 3, 4:
assert.Equal(t, time.UnixMilli(1698740324437), v.LastUpdated, "datetime")
assert.Equal(t, 0.00008486000000000000, v.High, "high")
assert.Equal(t, 0.00008318, v.Last, "lastTradedPrice")
assert.Equal(t, 0.00007152000000000000, v.Low, "low")
assert.Equal(t, 2958.3139116, v.Volume, "volume")
assert.Equal(t, 160.7847672784213, v.QuoteVolume, "volValue")
case 4:
assert.Equal(t, asset.Spot, v.AssetType, "AssetType")
assert.Equal(t, time.UnixMilli(1700555342151), v.LastUpdated, "datetime")
assert.Equal(t, 37750.0, v.High, "high")
assert.Equal(t, 37366.8, v.Last, "lastTradedPrice")
assert.Equal(t, 36700.0, v.Low, "low")
assert.Equal(t, currency.NewPairWithDelimiter("BTC", "USDT", "-"), v.Pair, "symbol")
assert.Equal(t, 17062.45450000000000000000, v.Volume, "volume")
assert.Equal(t, 1.33076678861000000000, v.QuoteVolume, "volValue")
assert.Equal(t, 2900.37846402, v.Volume, "volume")
assert.Equal(t, 108210331.34015164, v.QuoteVolume, "volValue")
default:
t.Errorf("Got an unexpected *ticker.Price: %v", v)
}
case error:
t.Error(v)
@@ -2410,13 +2526,11 @@ func TestProcessMarketSnapshot(t *testing.T) {
func TestSubscribeMarketSnapshot(t *testing.T) {
t.Parallel()
s := []stream.ChannelSubscription{
{Channel: marketTickerSnapshotForCurrencyChannel,
Currency: currency.Pair{Base: currency.BTC}},
}
err := ku.Subscribe(s)
setupWS()
err := ku.Subscribe([]subscription.Subscription{{Channel: marketSymbolSnapshotChannel, Pair: currency.Pair{Base: currency.BTC}}})
assert.NoError(t, err, "Subscribe to MarketSnapshot should not error")
}
func TestSeedLocalCache(t *testing.T) {
t.Parallel()
pair, err := currency.NewPairFromString("ETH-USDT")
@@ -2599,14 +2713,18 @@ func TestUpdateOrderExecutionLimits(t *testing.T) {
func TestGetOpenInterest(t *testing.T) {
t.Parallel()
_, err := ku.GetOpenInterest(context.Background(), key.PairAsset{
nu := new(Kucoin)
require.NoError(t, testexch.TestInstance(nu), "TestInstance setup should not error")
_, err := nu.GetOpenInterest(context.Background(), key.PairAsset{
Base: currency.ETH.Item,
Quote: currency.USDT.Item,
Asset: asset.USDTMarginedFutures,
})
assert.ErrorIs(t, err, asset.ErrNotSupported)
resp, err := ku.GetOpenInterest(context.Background(), key.PairAsset{
resp, err := nu.GetOpenInterest(context.Background(), key.PairAsset{
Base: futuresTradablePair.Base.Item,
Quote: futuresTradablePair.Quote.Item,
Asset: asset.Futures,
@@ -2615,8 +2733,8 @@ func TestGetOpenInterest(t *testing.T) {
assert.NotEmpty(t, resp)
cp1 := currency.NewPair(currency.ETH, currency.USDTM)
sharedtestvalues.SetupCurrencyPairsForExchangeAsset(t, ku, asset.Futures, cp1)
resp, err = ku.GetOpenInterest(context.Background(),
sharedtestvalues.SetupCurrencyPairsForExchangeAsset(t, nu, asset.Futures, cp1)
resp, err = nu.GetOpenInterest(context.Background(),
key.PairAsset{
Base: futuresTradablePair.Base.Item,
Quote: futuresTradablePair.Quote.Item,
@@ -2631,7 +2749,7 @@ func TestGetOpenInterest(t *testing.T) {
assert.NoError(t, err)
assert.NotEmpty(t, resp)
resp, err = ku.GetOpenInterest(context.Background())
resp, err = nu.GetOpenInterest(context.Background())
assert.NoError(t, err)
assert.NotEmpty(t, resp)
}

View File

@@ -34,6 +34,8 @@ var (
errSizeOrFundIsRequired = errors.New("at least one required among size and funds")
errInvalidLeverage = errors.New("invalid leverage value")
errInvalidClientOrderID = errors.New("no client order ID supplied, this endpoint requires a UUID or similar string")
errInvalidMsgType = errors.New("message type field not valid")
errSubscriptionPairRequired = errors.New("pair required for manual subscriptions")
subAccountRegExp = regexp.MustCompile("^[a-zA-Z0-9]{7-32}$")
subAccountPassphraseRegExp = regexp.MustCompile("^[a-zA-Z0-9]{7-24}$")

View File

@@ -11,17 +11,18 @@ import (
"sync"
"time"
"github.com/buger/jsonparser"
"github.com/gorilla/websocket"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/currency"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/account"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"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"
@@ -34,21 +35,20 @@ const (
privateBullets = "/v1/bullet-private"
// spot channels
marketTickerChannel = "/market/ticker:%s" // /market/ticker:{symbol},{symbol}...
marketAllTickersChannel = "/market/ticker:all"
marketTickerSnapshotChannel = "/market/snapshot:%s" // /market/snapshot:{symbol}
marketTickerSnapshotForCurrencyChannel = "/market/snapshot:" // /market/snapshot:{market} <--- market represents a currency
marketOrderbookLevel2Channels = "/market/level2:%s" // /market/level2:{symbol},{symbol}...
marketOrderbookLevel2to5Channel = "/spotMarket/level2Depth5:%s" // /spotMarket/level2Depth5:{symbol},{symbol}...
marketOrderbokLevel2To50Channel = "/spotMarket/level2Depth50:%s" // /spotMarket/level2Depth50:{symbol},{symbol}...
marketCandlesChannel = "/market/candles:%s_%s" // /market/candles:{symbol}_{type}
marketMatchChannel = "/market/match:%s" // /market/match:{symbol},{symbol}...
indexPriceIndicatorChannel = "/indicator/index:%s" // /indicator/index:{symbol0},{symbol1}..
markPriceIndicatorChannel = "/indicator/markPrice:%s" // /indicator/markPrice:{symbol0},{symbol1}...
marginFundingbookChangeChannel = "/margin/fundingBook:%s" // /margin/fundingBook:{currency0},{currency1}...
// Private channel
marketAllTickersChannel = "/market/ticker:all"
marketTickerChannel = "/market/ticker:%s" // /market/ticker:{symbol},{symbol}...
marketSymbolSnapshotChannel = "/market/snapshot:%s" // /market/snapshot:{symbol}
marketSnapshotChannel = "/market/snapshot:%v" // /market/snapshot:{market} <--- market represents a currency
marketOrderbookLevel2Channels = "/market/level2:%s" // /market/level2:{pair},{pair}...
marketOrderbookLevel2to5Channel = "/spotMarket/level2Depth5:%s" // /spotMarket/level2Depth5:{symbol},{symbol}...
marketOrderbokLevel2To50Channel = "/spotMarket/level2Depth50:%s" // /spotMarket/level2Depth50:{symbol},{symbol}...
marketCandlesChannel = "/market/candles:%s_%s" // /market/candles:{symbol}_{interval}
marketMatchChannel = "/market/match:%s" // /market/match:{symbol},{symbol}...
indexPriceIndicatorChannel = "/indicator/index:%s" // /indicator/index:{symbol0},{symbol1}..
markPriceIndicatorChannel = "/indicator/markPrice:%s" // /indicator/markPrice:{symbol0},{symbol1}...
marginFundingbookChangeChannel = "/margin/fundingBook:%s" // /margin/fundingBook:{currency0},{currency1}...
// Private channels
privateSpotTradeOrders = "/spotMarket/tradeOrders"
accountBalanceChannel = "/account/balance"
marginPositionChannel = "/margin/position"
@@ -56,7 +56,6 @@ const (
spotMarketAdvancedChannel = "/spotMarket/advancedOrders"
// futures channels
futuresTickerV2Channel = "/contractMarket/tickerV2:%s" // /contractMarket/tickerV2:{symbol}
futuresTickerChannel = "/contractMarket/ticker:%s" // /contractMarket/ticker:{symbol}
futuresOrderbookLevel2Channel = "/contractMarket/level2:%s" // /contractMarket/level2:{symbol}
@@ -68,7 +67,6 @@ const (
futuresTrasactionStatisticsTimerEventChannel = "/contractMarket/snapshot:%s" // /contractMarket/snapshot:{symbol}
// futures private channels
futuresTradeOrdersBySymbolChannel = "/contractMarket/tradeOrders:%s" // /contractMarket/tradeOrders:{symbol}
futuresTradeOrderChannel = "/contractMarket/tradeOrders"
futuresStopOrdersLifecycleEventChannel = "/contractMarket/advancedOrders"
@@ -76,6 +74,14 @@ const (
futuresPositionChangeEventChannel = "/contract/position:%s" // /contract/position:{symbol}
)
var subscriptionNames = map[string]string{
subscription.TickerChannel: marketTickerChannel,
subscription.OrderbookChannel: marketOrderbookLevel2Channels,
subscription.CandlesChannel: marketCandlesChannel,
subscription.AllTradesChannel: marketMatchChannel,
// No equivalents for: AllOrders, MyTrades, MyOrders
}
var (
// maxWSUpdateBuffer defines max websocket updates to apply when an
// orderbook is initially fetched
@@ -88,22 +94,6 @@ var (
maxWSOrderbookWorkers = 10
)
var requiredSubscriptionIDS map[string]bool
var requiredSubscriptionIDSLock sync.Mutex
// checkRequiredSubscriptionID check whether the id included in the required subscription ids list.
func (ku *Kucoin) checkRequiredSubscriptionID(id string) bool {
if len(requiredSubscriptionIDS) > 0 {
if requiredSubscriptionIDS[id] {
requiredSubscriptionIDSLock.Lock()
delete(requiredSubscriptionIDS, id)
requiredSubscriptionIDSLock.Unlock()
return true
}
}
return false
}
// WsConnect creates a new websocket connection.
func (ku *Kucoin) WsConnect() error {
if !ku.Websocket.IsEnabled() || !ku.IsEnabled() {
@@ -208,17 +198,16 @@ func (ku *Kucoin) wsHandleData(respData []byte) error {
err := json.Unmarshal(respData, &resp)
if err != nil {
return err
} else if resp.ID != "" {
if ku.checkRequiredSubscriptionID(resp.ID) {
if !ku.Websocket.Match.IncomingWithData(resp.ID, respData) {
return fmt.Errorf("can not match subscription message with signature ID:%s", resp.ID)
}
}
return nil
}
if resp.Type == "pong" || resp.Type == "welcome" {
return nil
}
if resp.ID != "" {
if !ku.Websocket.Match.IncomingWithData("msgID:"+resp.ID, respData) {
return fmt.Errorf("message listener not found: %s", resp.ID)
}
return nil
}
topicInfo := strings.Split(resp.Topic, ":")
switch {
case strings.HasPrefix(marketAllTickersChannel, topicInfo[0]),
@@ -230,8 +219,7 @@ func (ku *Kucoin) wsHandleData(respData []byte) error {
instruments = topicInfo[1]
}
return ku.processTicker(resp.Data, instruments)
case strings.HasPrefix(marketTickerSnapshotChannel, topicInfo[0]) ||
strings.HasPrefix(marketTickerSnapshotForCurrencyChannel, topicInfo[0]):
case strings.HasPrefix(marketSymbolSnapshotChannel, topicInfo[0]):
return ku.processMarketSnapshot(resp.Data)
case strings.HasPrefix(marketOrderbookLevel2Channels, topicInfo[0]):
return ku.processOrderbookWithDepth(respData, topicInfo[1])
@@ -916,396 +904,250 @@ func (ku *Kucoin) processMarketSnapshot(respData []byte) error {
}
// Subscribe sends a websocket message to receive data from the channel
func (ku *Kucoin) Subscribe(subscriptions []stream.ChannelSubscription) error {
func (ku *Kucoin) Subscribe(subscriptions []subscription.Subscription) error {
return ku.handleSubscriptions(subscriptions, "subscribe")
}
// Unsubscribe sends a websocket message to stop receiving data from the channel
func (ku *Kucoin) Unsubscribe(subscriptions []stream.ChannelSubscription) error {
func (ku *Kucoin) Unsubscribe(subscriptions []subscription.Subscription) error {
return ku.handleSubscriptions(subscriptions, "unsubscribe")
}
func (ku *Kucoin) handleSubscriptions(subscriptions []stream.ChannelSubscription, operation string) error {
if requiredSubscriptionIDS == nil {
requiredSubscriptionIDS = map[string]bool{}
}
payloads, err := ku.generatePayloads(subscriptions, operation)
if err != nil {
return err
}
var errs error
for x := range payloads {
err = ku.Websocket.Conn.SendJSONMessage(payloads[x])
if err != nil {
errs = common.AppendError(errs, err)
continue
func (ku *Kucoin) expandManualSubscriptions(in []subscription.Subscription) ([]subscription.Subscription, error) {
subs := make([]subscription.Subscription, 0, len(in))
for i := range in {
if isSymbolChannel(in[i].Channel) {
if in[i].Pair.IsEmpty() {
return nil, errSubscriptionPairRequired
}
a := in[i].Asset
if !a.IsValid() {
a = getChannelsAssetType(in[i].Channel)
}
assetPairs := map[asset.Item]currency.Pairs{a: {in[i].Pair}}
n, err := ku.expandSubscription(&in[i], assetPairs)
if err != nil {
return nil, err
}
subs = append(subs, n...)
} else {
subs = append(subs, in[i])
}
}
return subs, nil
}
func (ku *Kucoin) handleSubscriptions(subs []subscription.Subscription, operation string) error {
var errs error
subs, errs = ku.expandManualSubscriptions(subs)
for i := range subs {
msgID := strconv.FormatInt(ku.Websocket.Conn.GenerateMessageID(false), 10)
req := WsSubscriptionInput{
ID: msgID,
Type: operation,
Topic: subs[i].Channel,
PrivateChannel: subs[i].Authenticated,
Response: true,
}
if respRaw, err := ku.Websocket.Conn.SendMessageReturnResponse("msgID:"+msgID, req); err != nil {
errs = common.AppendError(errs, err)
} else {
rType, err := jsonparser.GetUnsafeString(respRaw, "type")
switch {
case err != nil:
errs = common.AppendError(errs, err)
case rType != "ack":
errs = common.AppendError(errs, fmt.Errorf("%w: %s from %s", errInvalidMsgType, rType, respRaw))
default:
ku.Websocket.AddSuccessfulSubscriptions(subs[i])
if ku.Verbose {
log.Debugf(log.ExchangeSys, "%s Subscribed to Channel: %s", ku.Name, subs[i].Channel)
}
}
}
ku.Websocket.AddSuccessfulSubscriptions(subscriptions[x])
}
return errs
}
// getChannelsAssetType returns the asset type to which the subscription channel belongs to
// or returns an error otherwise.
func (ku *Kucoin) getChannelsAssetType(channelName string) (asset.Item, error) {
// getChannelsAssetType returns the asset type to which the subscription channel belongs to or asset.Empty
func getChannelsAssetType(channelName string) asset.Item {
switch channelName {
case futuresTickerV2Channel, futuresTickerChannel, futuresOrderbookLevel2Channel, futuresExecutionDataChannel, futuresOrderbookLevel2Depth5Channel, futuresOrderbookLevel2Depth50Channel, futuresContractMarketDataChannel, futuresSystemAnnouncementChannel, futuresTrasactionStatisticsTimerEventChannel, futuresTradeOrdersBySymbolChannel, futuresTradeOrderChannel, futuresStopOrdersLifecycleEventChannel, futuresAccountBalanceEventChannel, futuresPositionChangeEventChannel:
return asset.Futures, nil
return asset.Futures
case marketTickerChannel, marketAllTickersChannel,
marketTickerSnapshotChannel, marketTickerSnapshotForCurrencyChannel,
marketSnapshotChannel, marketSymbolSnapshotChannel,
marketOrderbookLevel2Channels, marketOrderbookLevel2to5Channel,
marketOrderbokLevel2To50Channel, marketCandlesChannel,
marketMatchChannel, indexPriceIndicatorChannel,
markPriceIndicatorChannel, marginFundingbookChangeChannel,
privateSpotTradeOrders, accountBalanceChannel,
marginPositionChannel, marginLoanChannel,
spotMarketAdvancedChannel:
return asset.Spot, nil
marketMatchChannel, indexPriceIndicatorChannel, markPriceIndicatorChannel,
privateSpotTradeOrders, accountBalanceChannel, spotMarketAdvancedChannel:
return asset.Spot
case marginFundingbookChangeChannel, marginPositionChannel, marginLoanChannel:
return asset.Margin
default:
return asset.Empty, errors.New("channel not supported")
return asset.Empty
}
}
// GenerateDefaultSubscriptions Adds default subscriptions to websocket.
func (ku *Kucoin) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, error) {
channels := []string{}
if ku.CurrencyPairs.IsAssetEnabled(asset.Spot) == nil || ku.CurrencyPairs.IsAssetEnabled(asset.Margin) == nil {
channels = append(channels,
marketTickerChannel,
marketMatchChannel,
marketOrderbookLevel2Channels)
}
if ku.CurrencyPairs.IsAssetEnabled(asset.Margin) == nil {
channels = append(channels,
marginFundingbookChangeChannel)
}
if ku.CurrencyPairs.IsAssetEnabled(asset.Futures) == nil {
channels = append(channels,
futuresTickerV2Channel,
futuresOrderbookLevel2Depth50Channel)
}
var subscriptions []stream.ChannelSubscription
if ku.Websocket.CanUseAuthenticatedEndpoints() {
if ku.CurrencyPairs.IsAssetEnabled(asset.Spot) == nil {
channels = append(channels,
accountBalanceChannel,
)
}
if ku.CurrencyPairs.IsAssetEnabled(asset.Margin) == nil {
channels = append(channels,
marginPositionChannel,
marginLoanChannel,
)
}
if ku.CurrencyPairs.IsAssetEnabled(asset.Futures) == nil {
channels = append(channels,
// futures authenticated channels
futuresTradeOrdersBySymbolChannel,
futuresTradeOrderChannel,
futuresStopOrdersLifecycleEventChannel,
futuresAccountBalanceEventChannel)
func (ku *Kucoin) GenerateDefaultSubscriptions() ([]subscription.Subscription, error) {
assetPairs := map[asset.Item]currency.Pairs{}
for _, a := range ku.GetAssetTypes(false) {
if p, err := ku.GetEnabledPairs(a); err == nil {
assetPairs[a] = p
} else {
assetPairs[a] = currency.Pairs{} // err is probably that Asset isn't enabled, but we don't care about errors of any type
}
}
var err error
var spotPairs currency.Pairs
if ku.CurrencyPairs.IsAssetEnabled(asset.Spot) == nil {
spotPairs, err = ku.GetEnabledPairs(asset.Spot)
authed := ku.Websocket.CanUseAuthenticatedEndpoints()
subscriptions := []subscription.Subscription{}
for _, s := range ku.Features.Subscriptions {
if !authed && s.Authenticated {
continue
}
subs, err := ku.expandSubscription(s, assetPairs)
if err != nil {
return nil, err
}
}
var marginPairs currency.Pairs
if ku.CurrencyPairs.IsAssetEnabled(asset.Margin) == nil {
marginPairs, err = ku.GetEnabledPairs(asset.Margin)
if err != nil {
return nil, err
}
}
var futuresPairs currency.Pairs
if ku.CurrencyPairs.IsAssetEnabled(asset.Futures) == nil {
futuresPairs, err = ku.GetEnabledPairs(asset.Futures)
if err != nil {
return nil, err
}
}
marginLoanCurrencyCheckMap := map[currency.Code]bool{}
for x := range channels {
switch channels[x] {
case accountBalanceChannel, marginPositionChannel,
futuresTradeOrderChannel, futuresStopOrdersLifecycleEventChannel,
spotMarketAdvancedChannel, privateSpotTradeOrders,
marketAllTickersChannel, futuresSystemAnnouncementChannel,
futuresAccountBalanceEventChannel:
subscriptions = append(subscriptions, stream.ChannelSubscription{
Channel: channels[x],
})
case marketTickerSnapshotChannel,
marketOrderbookLevel2Channels,
marketTickerSnapshotForCurrencyChannel,
marketOrderbookLevel2to5Channel,
marketOrderbokLevel2To50Channel,
marketTickerChannel:
subscribedPairsMap := map[string]bool{}
for b := range spotPairs {
if okay := subscribedPairsMap[spotPairs[b].String()]; okay {
continue
}
subscriptions = append(subscriptions, stream.ChannelSubscription{
Channel: channels[x],
Asset: asset.Spot,
Currency: spotPairs[b],
})
subscribedPairsMap[spotPairs[b].String()] = true
}
for b := range marginPairs {
if okay := subscribedPairsMap[marginPairs[b].String()]; okay {
continue
}
subscriptions = append(subscriptions, stream.ChannelSubscription{
Channel: channels[x],
Asset: asset.Margin,
Currency: marginPairs[b],
})
subscribedPairsMap[marginPairs[b].String()] = true
}
case indexPriceIndicatorChannel,
markPriceIndicatorChannel,
marketMatchChannel:
pairs := currency.Pairs{}
for p := range spotPairs {
pairs = pairs.Add(spotPairs[p])
}
for p := range marginPairs {
pairs = pairs.Add(marginPairs[p])
}
subscriptions = append(subscriptions, stream.ChannelSubscription{
Channel: channels[x],
Asset: asset.Spot,
Params: map[string]interface{}{"symbols": pairs.Join()},
})
case marketCandlesChannel:
subscribedPairsMap := map[string]bool{}
for p := range spotPairs {
if okay := subscribedPairsMap[spotPairs[p].String()]; okay {
continue
}
subscriptions = append(subscriptions, stream.ChannelSubscription{
Channel: channels[x],
Asset: asset.Spot,
Currency: spotPairs[p],
Params: map[string]interface{}{"interval": kline.FifteenMin},
})
subscribedPairsMap[spotPairs[p].String()] = true
}
for p := range marginPairs {
if okay := subscribedPairsMap[marginPairs[p].String()]; okay {
continue
}
subscriptions = append(subscriptions, stream.ChannelSubscription{
Channel: channels[x],
Asset: asset.Margin,
Currency: marginPairs[p],
Params: map[string]interface{}{"interval": kline.FifteenMin},
})
subscribedPairsMap[marginPairs[p].String()] = true
}
case marginLoanChannel:
for b := range marginPairs {
if !marginLoanCurrencyCheckMap[marginPairs[b].Quote] {
subscriptions = append(subscriptions, stream.ChannelSubscription{
Channel: channels[x],
Currency: currency.Pair{Base: marginPairs[b].Quote},
})
marginLoanCurrencyCheckMap[marginPairs[b].Quote] = true
}
if !marginLoanCurrencyCheckMap[marginPairs[b].Base] {
subscriptions = append(subscriptions, stream.ChannelSubscription{
Channel: channels[x],
Currency: currency.Pair{Base: marginPairs[b].Base},
})
marginLoanCurrencyCheckMap[marginPairs[b].Base] = true
}
}
case marginFundingbookChangeChannel:
currencyExist := map[currency.Code]bool{}
for b := range marginPairs {
okay := currencyExist[marginPairs[b].Base]
if !okay {
currencyExist[marginPairs[b].Base] = true
}
okay = currencyExist[marginPairs[b].Quote]
if !okay {
currencyExist[marginPairs[b].Quote] = true
}
}
var currencies string
for b := range currencyExist {
currencies += b.String() + ","
}
currencies = strings.TrimSuffix(currencies, ",")
subscriptions = append(subscriptions, stream.ChannelSubscription{
Channel: channels[x],
Params: map[string]interface{}{"currencies": currencies},
})
case futuresTickerV2Channel,
futuresTickerChannel,
futuresExecutionDataChannel,
futuresOrderbookLevel2Channel,
futuresOrderbookLevel2Depth5Channel,
futuresOrderbookLevel2Depth50Channel,
futuresContractMarketDataChannel,
futuresTradeOrdersBySymbolChannel,
futuresPositionChangeEventChannel,
futuresTrasactionStatisticsTimerEventChannel:
for b := range futuresPairs {
futuresPairs[b], err = ku.FormatExchangeCurrency(futuresPairs[b], asset.Futures)
if err != nil {
continue
}
subscriptions = append(subscriptions, stream.ChannelSubscription{
Channel: channels[x],
Asset: asset.Futures,
Currency: futuresPairs[b],
})
}
}
subscriptions = append(subscriptions, subs...)
}
return subscriptions, nil
}
func (ku *Kucoin) generatePayloads(subscriptions []stream.ChannelSubscription, operation string) ([]WsSubscriptionInput, error) {
payloads := make([]WsSubscriptionInput, 0, len(subscriptions))
marketTickerSnapshotForCurrencyChannelCurrencyFilter := map[currency.Code]int{}
for x := range subscriptions {
var err error
var a asset.Item
a, err = ku.getChannelsAssetType(subscriptions[x].Channel)
// expandSubscription takes a subscription and expands it across the relevant assets and pairs passed in
func (ku *Kucoin) expandSubscription(baseSub *subscription.Subscription, assetPairs map[asset.Item]currency.Pairs) ([]subscription.Subscription, error) {
var subscriptions = []subscription.Subscription{}
if baseSub == nil {
return nil, common.ErrNilPointer
}
s := *baseSub
s.Channel = channelName(s.Channel)
if !s.Asset.IsValid() {
s.Asset = getChannelsAssetType(s.Channel)
}
switch {
case s.Channel == marginLoanChannel:
for _, c := range assetPairs[asset.Margin].GetCurrencies() {
i := s
i.Channel = fmt.Sprintf(s.Channel, c)
subscriptions = append(subscriptions, i)
}
case s.Channel == marketCandlesChannel:
interval, err := ku.intervalToString(s.Interval)
if err != nil {
return nil, err
}
if !subscriptions[x].Currency.IsEmpty() {
subscriptions[x].Currency, err = ku.FormatExchangeCurrency(subscriptions[x].Currency, a)
if err != nil {
return nil, err
}
subs := spotOrMarginPairSubs(assetPairs, &s, false, interval)
subscriptions = append(subscriptions, subs...)
case s.Channel == marginFundingbookChangeChannel:
s.Channel = fmt.Sprintf(s.Channel, assetPairs[asset.Margin].GetCurrencies().Join())
subscriptions = append(subscriptions, s)
case s.Channel == marketSnapshotChannel:
subs, err := spotOrMarginCurrencySubs(assetPairs, &s)
if err != nil {
return nil, err
}
if subscriptions[x].Asset == asset.Futures {
subscriptions[x].Currency, err = ku.FormatExchangeCurrency(subscriptions[x].Currency, asset.Futures)
subscriptions = append(subscriptions, subs...)
case getChannelsAssetType(s.Channel) == asset.Futures && isSymbolChannel(s.Channel):
for _, p := range assetPairs[asset.Futures] {
c, err := ku.FormatExchangeCurrency(p, asset.Futures)
if err != nil {
continue
}
i := s
i.Channel = fmt.Sprintf(s.Channel, c)
subscriptions = append(subscriptions, i)
}
switch subscriptions[x].Channel {
case marketTickerChannel,
marketOrderbookLevel2Channels,
marketOrderbookLevel2to5Channel,
marketOrderbokLevel2To50Channel,
indexPriceIndicatorChannel,
marketMatchChannel,
markPriceIndicatorChannel:
symbols, okay := subscriptions[x].Params["symbols"].(string)
if !okay {
if subscriptions[x].Currency.IsEmpty() {
return nil, errors.New("symbols not passed")
}
symbols = subscriptions[x].Currency.String()
case isSymbolChannel(s.Channel):
// Subscriptions which can use a single comma-separated sub per asset
subs := spotOrMarginPairSubs(assetPairs, &s, true)
subscriptions = append(subscriptions, subs...)
default:
subscriptions = append(subscriptions, s)
}
return subscriptions, nil
}
// isSymbolChannel returns true it this channel path ends in a formatting %s to accept a Symbol
func isSymbolChannel(c string) bool {
return strings.HasSuffix(c, "%s") || strings.HasSuffix(c, "%v")
}
// channelName converts global channel Names used in config of channel input into kucoin channel names
// returns the name unchanged if no match is found
func channelName(name string) string {
if s, ok := subscriptionNames[name]; ok {
return s
}
return name
}
// spotOrMarginPairSubs accepts a map of pairs and a template subscription and returns a list of subscriptions for Spot and Margin pairs
// If there's a Spot subscription, it won't be added again as a Margin subscription
// If joined param is true then one subscription per asset type with the currencies comma delimited
func spotOrMarginPairSubs(assetPairs map[asset.Item]currency.Pairs, b *subscription.Subscription, join bool, fmtArgs ...any) []subscription.Subscription {
subs := []subscription.Subscription{}
add := func(a asset.Item, pairs currency.Pairs) {
if len(pairs) == 0 {
return
}
s := *b
s.Asset = a
if join {
f := append([]any{pairs.Join()}, fmtArgs...)
s.Channel = fmt.Sprintf(b.Channel, f...)
subs = append(subs, s)
} else {
for i := range pairs {
f := append([]any{pairs[i].String()}, fmtArgs...)
s.Channel = fmt.Sprintf(b.Channel, f...)
subs = append(subs, s)
}
payloads = append(payloads, WsSubscriptionInput{
ID: strconv.FormatInt(ku.Websocket.Conn.GenerateMessageID(false), 10),
Type: operation,
Topic: fmt.Sprintf(subscriptions[x].Channel, symbols),
Response: true,
})
case marketAllTickersChannel,
privateSpotTradeOrders,
accountBalanceChannel,
marginPositionChannel,
spotMarketAdvancedChannel,
futuresTradeOrderChannel,
futuresStopOrdersLifecycleEventChannel,
futuresAccountBalanceEventChannel, futuresSystemAnnouncementChannel:
input := WsSubscriptionInput{
ID: strconv.FormatInt(ku.Websocket.Conn.GenerateMessageID(false), 10),
Type: operation,
Topic: subscriptions[x].Channel,
Response: true,
}
switch subscriptions[x].Channel {
case futuresTradeOrderChannel,
futuresStopOrdersLifecycleEventChannel,
futuresAccountBalanceEventChannel,
privateSpotTradeOrders,
accountBalanceChannel,
marginPositionChannel,
spotMarketAdvancedChannel:
input.PrivateChannel = true
}
payloads = append(payloads, input)
case marketTickerSnapshotChannel, futuresPositionChangeEventChannel,
futuresTradeOrdersBySymbolChannel, futuresTrasactionStatisticsTimerEventChannel,
futuresContractMarketDataChannel, futuresOrderbookLevel2Depth50Channel,
futuresOrderbookLevel2Depth5Channel, futuresExecutionDataChannel,
futuresOrderbookLevel2Channel, futuresTickerChannel,
futuresTickerV2Channel: // Symbols
item := WsSubscriptionInput{
ID: strconv.FormatInt(ku.Websocket.Conn.GenerateMessageID(false), 10),
Type: operation,
Topic: fmt.Sprintf(subscriptions[x].Channel, subscriptions[x].Currency.String()),
Response: true,
}
switch subscriptions[x].Channel {
case futuresPositionChangeEventChannel,
futuresTradeOrdersBySymbolChannel:
item.PrivateChannel = true
}
payloads = append(payloads, item)
case marketTickerSnapshotForCurrencyChannel,
marginLoanChannel:
// 3 means the Currency is used by both switch cases
// 2 means the currency is used by channel = marginLoanChannel
// 1 if used by marketTickerSnapshotForCurrencyChannel
if stat := marketTickerSnapshotForCurrencyChannelCurrencyFilter[subscriptions[x].Currency.Base]; stat == 3 || (stat == 2 && subscriptions[x].Channel == marginLoanChannel) || stat == 1 {
continue
}
input := WsSubscriptionInput{}
if subscriptions[x].Channel == marginLoanChannel {
input.PrivateChannel = true
marketTickerSnapshotForCurrencyChannelCurrencyFilter[subscriptions[x].Currency.Base] += 2
} else {
marketTickerSnapshotForCurrencyChannelCurrencyFilter[subscriptions[x].Currency.Base]++
subscriptions[x].Channel += "%s"
}
input.ID = strconv.FormatInt(ku.Websocket.Conn.GenerateMessageID(false), 10)
input.Type = operation
input.Topic = fmt.Sprintf(subscriptions[x].Channel, subscriptions[x].Currency.Base.Upper().String())
input.Response = true
payloads = append(payloads, input)
case marketCandlesChannel:
interval, err := ku.intervalToString(subscriptions[x].Params["interval"].(kline.Interval))
if err != nil {
return nil, err
}
payloads = append(payloads, WsSubscriptionInput{
ID: strconv.FormatInt(ku.Websocket.Conn.GenerateMessageID(false), 10),
Type: operation,
Topic: fmt.Sprintf(subscriptions[x].Channel, subscriptions[x].Currency.Upper().String(), interval),
Response: true,
})
case marginFundingbookChangeChannel:
currencies, okay := subscriptions[x].Params["currencies"].(string)
if !okay {
return nil, errors.New("currencies not passed")
}
payloads = append(payloads, WsSubscriptionInput{
ID: strconv.FormatInt(ku.Websocket.Conn.GenerateMessageID(false), 10),
Type: operation,
Topic: fmt.Sprintf(subscriptions[x].Channel, currencies),
Response: true,
})
}
}
return payloads, nil
add(asset.Spot, assetPairs[asset.Spot])
marginPairs := currency.Pairs{}
for _, p := range assetPairs[asset.Margin] {
if !assetPairs[asset.Spot].Contains(p, false) {
marginPairs = marginPairs.Add(p)
}
}
add(asset.Margin, marginPairs)
return subs
}
// spotOrMarginCurrencySubs accepts a map of pairs and a template subscription and returns a list of subscriptions for every currency in Spot and Margin pairs
// If there's a Spot subscription, it won't be added again as a Margin subscription
func spotOrMarginCurrencySubs(assetPairs map[asset.Item]currency.Pairs, b *subscription.Subscription) ([]subscription.Subscription, error) {
if b == nil {
return nil, common.ErrNilPointer
}
subs := []subscription.Subscription{}
add := func(a asset.Item, currs currency.Currencies) {
if len(currs) == 0 {
return
}
s := *b
s.Asset = a
for _, c := range currs {
s.Channel = fmt.Sprintf(b.Channel, c)
subs = append(subs, s)
}
}
add(asset.Spot, assetPairs[asset.Spot].GetCurrencies())
marginCurrencies := currency.Currencies{}
for _, c := range assetPairs[asset.Margin].GetCurrencies() {
if !assetPairs[asset.Spot].ContainsCurrency(c) {
marginCurrencies = marginCurrencies.Add(c)
}
}
add(asset.Margin, marginCurrencies)
return subs, nil
}
// orderbookManager defines a way of managing and maintaining synchronisation

View File

@@ -29,6 +29,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"
@@ -163,6 +164,21 @@ func (ku *Kucoin) SetDefaults() {
GlobalResultLimit: 1500,
},
},
Subscriptions: []*subscription.Subscription{
// Where we can we use generic names
{Enabled: true, Channel: subscription.TickerChannel}, // marketTickerChannel
{Enabled: true, Channel: subscription.AllTradesChannel}, // marketMatchChannel
{Enabled: true, Channel: subscription.OrderbookChannel, Interval: kline.HundredMilliseconds}, // marketOrderbookLevel2Channels
{Enabled: true, Channel: futuresTickerV2Channel},
{Enabled: true, Channel: futuresOrderbookLevel2Depth50Channel},
{Enabled: true, Channel: marginFundingbookChangeChannel, Authenticated: true},
{Enabled: true, Channel: accountBalanceChannel, Authenticated: true},
{Enabled: true, Channel: marginPositionChannel, Authenticated: true},
{Enabled: true, Channel: marginLoanChannel, Authenticated: true},
{Enabled: true, Channel: futuresTradeOrderChannel, Authenticated: true},
{Enabled: true, Channel: futuresStopOrdersLifecycleEventChannel, Authenticated: true},
{Enabled: true, Channel: futuresAccountBalanceEventChannel, Authenticated: true},
},
}
ku.Requester, err = request.New(ku.Name,
common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout),

View File

@@ -1,3 +1,3 @@
{"type":"message","topic":"/market/snapshot:BTC","subject":"trade.snapshot","data":{"sequence":1698740324504,"data":{"averagePrice":0.00001164,"baseCurrency":"XMR","board":0,"buy":0.00001252,"changePrice":0.00000104800000000000,"changeRate":0.0914,"close":0.000012508,"datetime":1698740324415,"high":0.00001402100000000000,"lastTradedPrice":0.000012508,"low":0.00001129200000000000,"makerCoefficient":2.000000,"makerFeeRate":0.001,"marginTrade":false,"mark":0,"market":"BTC","marketChange1h":{"changeRate":0,"high":0,"low":0,"open":0,"vol":0,"volValue":0},"marketChange24h":{"changePrice":0.00000104800000000000,"changeRate":0.0914,"high":0.00001402100000000000,"low":0.00001129200000000000,"open":0.00001146000000000000,"vol":28474.47280000000000000000,"volValue":0.37038038297340000000},"marketChange4h":{"changePrice":0.00000009600000000000,"changeRate":0.0077,"high":0.00001308400000000000,"low":0.00001241200000000000,"open":0.00001241200000000000,"vol":7090.00000000000000000000,"volValue":0.08885800028840000000},"markets":["BTC"],"open":0.00001146000000000000,"quoteCurrency":"BTC","sell":0.000013191,"sort":100,"symbol":"XMR-BTC","symbolCode":"XMR-BTC","takerCoefficient":2.000000,"takerFeeRate":0.001,"trading":true,"vol":28474.47280000000000000000,"volValue":0.37038038297340000000}}}
{"type":"message","topic":"/market/snapshot:BTC","subject":"trade.snapshot","data":{"sequence":1698740324488,"data":{"averagePrice":0.00000037,"baseCurrency":"ETH","board":0,"buy":0.0000003641,"changePrice":0.00000004770000000000,"changeRate":0.1394,"close":0.0000003897,"datetime":1698740324483,"high":0.00000039450000000000,"lastTradedPrice":0.0000003897,"low":0.00000034200000000000,"makerCoefficient":2.000000,"makerFeeRate":0.001,"marginTrade":false,"mark":0,"market":"BTC","marketChange1h":{"changeRate":0,"high":0,"low":0,"open":0,"vol":0,"volValue":0},"marketChange24h":{"changePrice":0.00000004770000000000,"changeRate":0.1394,"high":0.00000039450000000000,"low":0.00000034200000000000,"open":0.00000034200000000000,"vol":316078.69700000000000000000,"volValue":0.11768519138877000000},"marketChange4h":{"changePrice":0.00000003290000000000,"changeRate":0.0922,"high":0.00000038970000000000,"low":0.00000035680000000000,"open":0.00000035680000000000,"vol":2309.46880000000000000000,"volValue":0.00089999999136000000},"markets":["BTC"],"open":0.00000034200000000000,"quoteCurrency":"BTC","sell":0.0000004022,"sort":100,"symbol":"ETH-BTC","symbolCode":"ETH-BTC","takerCoefficient":2.000000,"takerFeeRate":0.001,"trading":true,"vol":316078.69700000000000000000,"volValue":0.11768519138877000000}}}
{"type":"message","topic":"/market/snapshot:BTC","subject":"trade.snapshot","data":{"sequence":1698740324508,"data":{"averagePrice":0.00007307,"baseCurrency":"BTC","board":0,"buy":0.00008388,"changePrice":0.00001166000000000000,"changeRate":0.1630,"close":0.00008318,"datetime":1698740324437,"high":0.00008486000000000000,"lastTradedPrice":0.00008318,"low":0.00007152000000000000,"makerCoefficient":1.000000,"makerFeeRate":0.001,"marginTrade":false,"mark":0,"market":"USDT","marketChange1h":{"changePrice":-0.00000116000000000000,"changeRate":-0.0137,"high":0.00008434000000000000,"low":0.00008318000000000000,"open":0.00008434000000000000,"vol":189.33430000000000000000,"volValue":0.01578748292300000000},"marketChange24h":{"changePrice":0.00001166000000000000,"changeRate":0.1630,"high":0.00008486000000000000,"low":0.00007152000000000000,"open":0.00007152000000000000,"vol":17062.45450000000000000000,"volValue":1.33076678861000000000},"marketChange4h":{"changePrice":0.00000143000000000000,"changeRate":0.0174,"high":0.00008486000000000000,"low":0.00008175000000000000,"open":0.00008175000000000000,"vol":1752.55690000000000000000,"volValue":0.14543003812900000000},"markets":["BTC"],"open":0.00007152000000000000,"quoteCurrency":"USDT","sell":0.00008421,"sort":100,"symbol":"BTC-USDT","symbolCode":"BTC-USDT","takerCoefficient":1.000000,"takerFeeRate":0.001,"trading":true,"vol":17062.45450000000000000000,"volValue":1.33076678861000000000}}}
{"type":"message","topic":"/market/snapshot:BTC","subject":"trade.snapshot","data":{"sequence":"459320318","data":{"averagePrice":0.00442293,"baseCurrency":"XMR","board":1,"buy":0.004411,"changePrice":0.00000000000000000000,"changeRate":0.0000,"close":0.004415,"datetime":1700555342007,"high":0.00444500000000000000,"lastTradedPrice":0.004415,"low":0.00419100000000000000,"makerCoefficient":1.000000,"makerFeeRate":0.001,"marginTrade":true,"mark":0,"market":"BTC","marketChange1h":{"changePrice":-0.00000200000000000000,"changeRate":-0.0004,"high":0.00443600000000000000,"low":0.00441300000000000000,"open":0.00441700000000000000,"vol":505.11910000000000000000,"volValue":2.23457327520000000000},"marketChange24h":{"changePrice":0.00000000000000000000,"changeRate":0.0000,"high":0.00444500000000000000,"low":0.00419100000000000000,"open":0.00441500000000000000,"vol":13097.33570000000000000000,"volValue":57.44552981000000000000},"marketChange4h":{"changePrice":0.00001100000000000000,"changeRate":0.0024,"high":0.00443600000000000000,"low":0.00439300000000000000,"open":0.00440400000000000000,"vol":2124.84330000000000000000,"volValue":9.37472351370000000000},"markets":["BTC"],"open":0.00441500000000000000,"quoteCurrency":"BTC","sell":0.004415,"sort":100,"symbol":"XMR-BTC","symbolCode":"XMR-BTC","takerCoefficient":1.000000,"takerFeeRate":0.001,"trading":true,"vol":13097.33570000000000000000,"volValue":57.44552981000000000000}}}
{"type":"message","topic":"/market/snapshot:BTC","subject":"trade.snapshot","data":{"sequence":"692562428","data":{"averagePrice":0.05414932,"baseCurrency":"ETH","board":1,"buy":0.053778,"changePrice":-0.00045800000000000000,"changeRate":-0.0084,"close":0.053778,"datetime":1700555340197,"high":0.05484600000000000000,"lastTradedPrice":0.053778,"low":0.05364000000000000000,"makerCoefficient":1.000000,"makerFeeRate":0.001,"marginTrade":true,"mark":0,"market":"BTC","marketChange1h":{"changePrice":-0.00008000000000000000,"changeRate":-0.0014,"high":0.05387400000000000000,"low":0.05371000000000000000,"open":0.05385800000000000000,"vol":63.72190510000000000000,"volValue":3.42879155215990000000},"marketChange24h":{"changePrice":-0.00045800000000000000,"changeRate":-0.0084,"high":0.05484600000000000000,"low":0.05364000000000000000,"open":0.05423600000000000000,"vol":2958.31391160000000000000,"volValue":160.78476727842130000000},"marketChange4h":{"changePrice":-0.00002700000000000000,"changeRate":-0.0005,"high":0.05399000000000000000,"low":0.05371000000000000000,"open":0.05380500000000000000,"vol":166.22099950000000000000,"volValue":8.95534024043750000000},"markets":["BTC","Shanghai-Upgrade"],"open":0.05423600000000000000,"quoteCurrency":"BTC","sell":0.053779,"sort":100,"symbol":"ETH-BTC","symbolCode":"ETH-BTC","takerCoefficient":1.000000,"takerFeeRate":0.001,"trading":true,"vol":2958.31391160000000000000,"volValue":160.78476727842130000000}}}
{"type":"message","topic":"/market/snapshot:BTC","subject":"trade.snapshot","data":{"sequence":"9860735911","data":{"averagePrice":37110.27939304,"baseCurrency":"BTC","board":1,"buy":37366.7,"changePrice":171.50000000000000000000,"changeRate":0.0046,"close":37366.8,"datetime":1700555342151,"high":37750.00000000000000000000,"lastTradedPrice":37366.8,"low":36700.00000000000000000000,"makerCoefficient":1.000000,"makerFeeRate":0.001,"marginTrade":true,"mark":0,"market":"USDS","marketChange1h":{"changePrice":105.60000000000000000000,"changeRate":0.0028,"high":37366.80000000000000000000,"low":37232.30000000000000000000,"open":37261.20000000000000000000,"vol":52.88854739000000000000,"volValue":1972678.83173137400000000000},"marketChange24h":{"changePrice":171.50000000000000000000,"changeRate":0.0046,"high":37750.00000000000000000000,"low":36700.00000000000000000000,"open":37195.30000000000000000000,"vol":2900.37846402000000000000,"volValue":108210331.34015163900000000000},"marketChange4h":{"changePrice":-94.70000000000000000000,"changeRate":-0.0025,"high":37476.40000000000000000000,"low":37232.30000000000000000000,"open":37461.50000000000000000000,"vol":263.14059486000000000000,"volValue":9829529.74388805200000000000},"markets":["USDS"],"open":37195.30000000000000000000,"quoteCurrency":"USDT","sell":37366.8,"sort":100,"symbol":"BTC-USDT","symbolCode":"BTC-USDT","takerCoefficient":1.000000,"takerFeeRate":0.001,"trading":true,"vol":2900.37846402000000000000,"volValue":108210331.34015163900000000000}}}

View File

@@ -21,6 +21,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"
@@ -581,9 +582,9 @@ func (o *Okcoin) wsProcessOrderbook(respRaw []byte, obChannel string) error {
// ReSubscribeSpecificOrderbook removes the subscription and the subscribes
// again to fetch a new snapshot in the event of a de-sync event.
func (o *Okcoin) ReSubscribeSpecificOrderbook(obChannel string, p currency.Pair) error {
subscription := []stream.ChannelSubscription{{
Channel: obChannel,
Currency: p,
subscription := []subscription.Subscription{{
Channel: obChannel,
Pair: p,
}}
if err := o.Unsubscribe(subscription); err != nil {
return err
@@ -763,8 +764,8 @@ func (o *Okcoin) CalculateOrderbookUpdateChecksum(orderbookData *orderbook.Base)
// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be
// handled by ManageSubscriptions()
func (o *Okcoin) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, error) {
var subscriptions []stream.ChannelSubscription
func (o *Okcoin) GenerateDefaultSubscriptions() ([]subscription.Subscription, error) {
var subscriptions []subscription.Subscription
pairs, err := o.GetEnabledPairs(asset.Spot)
if err != nil {
return nil, err
@@ -787,7 +788,7 @@ func (o *Okcoin) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, e
for s := range channels {
switch channels[s] {
case wsInstruments:
subscriptions = append(subscriptions, stream.ChannelSubscription{
subscriptions = append(subscriptions, subscription.Subscription{
Channel: channels[s],
Asset: asset.Spot,
})
@@ -798,20 +799,20 @@ func (o *Okcoin) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, e
wsCandle5m, wsCandle3m, wsCandle1m, wsCandle3Mutc, wsCandle1Mutc, wsCandle1Wutc, wsCandle1Dutc,
wsCandle2Dutc, wsCandle3Dutc, wsCandle5Dutc, wsCandle12Hutc, wsCandle6Hutc:
for p := range pairs {
subscriptions = append(subscriptions, stream.ChannelSubscription{
Channel: channels[s],
Currency: pairs[p],
subscriptions = append(subscriptions, subscription.Subscription{
Channel: channels[s],
Pair: pairs[p],
})
}
case wsStatus:
subscriptions = append(subscriptions, stream.ChannelSubscription{
subscriptions = append(subscriptions, subscription.Subscription{
Channel: channels[s],
})
case wsAccount:
currenciesMap := map[currency.Code]bool{}
for p := range pairs {
if reserved, okay := currenciesMap[pairs[p].Base]; !okay && !reserved {
subscriptions = append(subscriptions, stream.ChannelSubscription{
subscriptions = append(subscriptions, subscription.Subscription{
Channel: channels[s],
Params: map[string]interface{}{
"ccy": pairs[p].Base,
@@ -822,7 +823,7 @@ func (o *Okcoin) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, e
}
for p := range pairs {
if reserved, okay := currenciesMap[pairs[p].Quote]; !okay && !reserved {
subscriptions = append(subscriptions, stream.ChannelSubscription{
subscriptions = append(subscriptions, subscription.Subscription{
Channel: channels[s],
Params: map[string]interface{}{
"ccy": pairs[p].Quote,
@@ -833,10 +834,10 @@ func (o *Okcoin) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, e
}
case wsOrder, wsOrdersAlgo, wsAlgoAdvance:
for p := range pairs {
subscriptions = append(subscriptions, stream.ChannelSubscription{
Channel: channels[s],
Currency: pairs[p],
Asset: asset.Spot,
subscriptions = append(subscriptions, subscription.Subscription{
Channel: channels[s],
Pair: pairs[p],
Asset: asset.Spot,
})
}
default:
@@ -847,23 +848,23 @@ func (o *Okcoin) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, e
}
// Subscribe sends a websocket message to receive data from the channel
func (o *Okcoin) Subscribe(channelsToSubscribe []stream.ChannelSubscription) error {
func (o *Okcoin) Subscribe(channelsToSubscribe []subscription.Subscription) error {
return o.handleSubscriptions("subscribe", channelsToSubscribe)
}
// Unsubscribe sends a websocket message to stop receiving data from the channel
func (o *Okcoin) Unsubscribe(channelsToUnsubscribe []stream.ChannelSubscription) error {
func (o *Okcoin) Unsubscribe(channelsToUnsubscribe []subscription.Subscription) error {
return o.handleSubscriptions("unsubscribe", channelsToUnsubscribe)
}
func (o *Okcoin) handleSubscriptions(operation string, subs []stream.ChannelSubscription) error {
func (o *Okcoin) handleSubscriptions(operation string, subs []subscription.Subscription) error {
subscriptionRequest := WebsocketEventRequest{Operation: operation, Arguments: []map[string]string{}}
authRequest := WebsocketEventRequest{Operation: operation, Arguments: []map[string]string{}}
temp := WebsocketEventRequest{Operation: operation, Arguments: []map[string]string{}}
authTemp := WebsocketEventRequest{Operation: operation, Arguments: []map[string]string{}}
var err error
var channels []stream.ChannelSubscription
var authChannels []stream.ChannelSubscription
var channels []subscription.Subscription
var authChannels []subscription.Subscription
for i := 0; i < len(subs); i++ {
authenticatedChannelSubscription := isAuthenticatedChannel(subs[i].Channel)
// Temp type to evaluate max byte len after a marshal on batched unsubs
@@ -890,8 +891,8 @@ func (o *Okcoin) handleSubscriptions(operation string, subs []stream.ChannelSubs
if subs[i].Asset != asset.Empty {
argument["instType"] = strings.ToUpper(subs[i].Asset.String())
}
if !subs[i].Currency.IsEmpty() {
argument["instId"] = subs[i].Currency.String()
if !subs[i].Pair.IsEmpty() {
argument["instId"] = subs[i].Pair.String()
}
if authenticatedChannelSubscription {
authTemp.Arguments = append(authTemp.Arguments, argument)

View File

@@ -20,6 +20,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"
@@ -347,24 +348,24 @@ func (ok *Okx) wsReadData(ws stream.Connection) {
}
// Subscribe sends a websocket subscription request to several channels to receive data.
func (ok *Okx) Subscribe(channelsToSubscribe []stream.ChannelSubscription) error {
func (ok *Okx) Subscribe(channelsToSubscribe []subscription.Subscription) error {
return ok.handleSubscription(operationSubscribe, channelsToSubscribe)
}
// Unsubscribe sends a websocket unsubscription request to several channels to receive data.
func (ok *Okx) Unsubscribe(channelsToUnsubscribe []stream.ChannelSubscription) error {
func (ok *Okx) Unsubscribe(channelsToUnsubscribe []subscription.Subscription) error {
return ok.handleSubscription(operationUnsubscribe, channelsToUnsubscribe)
}
// handleSubscription sends a subscription and unsubscription information thought the websocket endpoint.
// as of the okx, exchange this endpoint sends subscription and unsubscription messages but with a list of json objects.
func (ok *Okx) handleSubscription(operation string, subscriptions []stream.ChannelSubscription) error {
func (ok *Okx) handleSubscription(operation string, subscriptions []subscription.Subscription) error {
request := WSSubscriptionInformationList{Operation: operation}
authRequests := WSSubscriptionInformationList{Operation: operation}
ok.WsRequestSemaphore <- 1
defer func() { <-ok.WsRequestSemaphore }()
var channels []stream.ChannelSubscription
var authChannels []stream.ChannelSubscription
var channels []subscription.Subscription
var authChannels []subscription.Subscription
var err error
var format currency.PairFormat
for i := 0; i < len(subscriptions); i++ {
@@ -431,10 +432,10 @@ func (ok *Okx) handleSubscription(operation string, subscriptions []stream.Chann
if err != nil {
return err
}
if subscriptions[i].Currency.Base.String() == "" || subscriptions[i].Currency.Quote.String() == "" {
if subscriptions[i].Pair.Base.String() == "" || subscriptions[i].Pair.Quote.String() == "" {
return errIncompleteCurrencyPair
}
instrumentID = format.Format(subscriptions[i].Currency)
instrumentID = format.Format(subscriptions[i].Pair)
}
}
if arg.Channel == okxChannelInstruments ||
@@ -454,7 +455,7 @@ func (ok *Okx) handleSubscription(operation string, subscriptions []stream.Chann
arg.Channel == okxChannelAlgoOrders ||
arg.Channel == okxChannelEstimatedPrice ||
arg.Channel == okxChannelOptSummary {
underlying, _ = ok.GetUnderlying(subscriptions[i].Currency, subscriptions[i].Asset)
underlying, _ = ok.GetUnderlying(subscriptions[i].Pair, subscriptions[i].Asset)
}
arg.InstrumentID = instrumentID
arg.Underlying = underlying
@@ -482,7 +483,7 @@ func (ok *Okx) handleSubscription(operation string, subscriptions []stream.Chann
} else {
ok.Websocket.AddSuccessfulSubscriptions(channels...)
}
authChannels = []stream.ChannelSubscription{}
authChannels = []subscription.Subscription{}
authRequests.Arguments = []SubscriptionInfo{}
}
} else {
@@ -504,7 +505,7 @@ func (ok *Okx) handleSubscription(operation string, subscriptions []stream.Chann
} else {
ok.Websocket.AddSuccessfulSubscriptions(channels...)
}
channels = []stream.ChannelSubscription{}
channels = []subscription.Subscription{}
request.Arguments = []SubscriptionInfo{}
continue
}
@@ -833,11 +834,11 @@ func (ok *Okx) wsProcessOrderBooks(data []byte) error {
}
if err != nil {
if errors.Is(err, errInvalidChecksum) {
err = ok.Subscribe([]stream.ChannelSubscription{
err = ok.Subscribe([]subscription.Subscription{
{
Channel: response.Argument.Channel,
Asset: assets[0],
Currency: pair,
Channel: response.Argument.Channel,
Asset: assets[0],
Pair: pair,
},
})
if err != nil {
@@ -1297,8 +1298,8 @@ func (ok *Okx) wsProcessTickers(data []byte) error {
}
// GenerateDefaultSubscriptions returns a list of default subscription message.
func (ok *Okx) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, error) {
var subscriptions []stream.ChannelSubscription
func (ok *Okx) GenerateDefaultSubscriptions() ([]subscription.Subscription, error) {
var subscriptions []subscription.Subscription
assets := ok.GetAssetTypes(true)
subs := make([]string, 0, len(defaultSubscribedChannels)+len(defaultAuthChannels))
subs = append(subs, defaultSubscribedChannels...)
@@ -1309,7 +1310,7 @@ func (ok *Okx) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, err
switch subs[c] {
case okxChannelOrders:
for x := range assets {
subscriptions = append(subscriptions, stream.ChannelSubscription{
subscriptions = append(subscriptions, subscription.Subscription{
Channel: subs[c],
Asset: assets[x],
})
@@ -1321,15 +1322,15 @@ func (ok *Okx) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, err
return nil, err
}
for p := range pairs {
subscriptions = append(subscriptions, stream.ChannelSubscription{
Channel: subs[c],
Asset: assets[x],
Currency: pairs[p],
subscriptions = append(subscriptions, subscription.Subscription{
Channel: subs[c],
Asset: assets[x],
Pair: pairs[p],
})
}
}
default:
subscriptions = append(subscriptions, stream.ChannelSubscription{
subscriptions = append(subscriptions, subscription.Subscription{
Channel: subs[c],
})
}

View File

@@ -19,6 +19,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"
@@ -540,36 +541,36 @@ func (p *Poloniex) WsProcessOrderbookUpdate(sequenceNumber float64, data []inter
}
// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions()
func (p *Poloniex) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, error) {
enabledCurrencies, err := p.GetEnabledPairs(asset.Spot)
func (p *Poloniex) GenerateDefaultSubscriptions() ([]subscription.Subscription, error) {
enabledPairs, err := p.GetEnabledPairs(asset.Spot)
if err != nil {
return nil, err
}
subscriptions := make([]stream.ChannelSubscription, 0, len(enabledCurrencies))
subscriptions = append(subscriptions, stream.ChannelSubscription{
subscriptions := make([]subscription.Subscription, 0, len(enabledPairs))
subscriptions = append(subscriptions, subscription.Subscription{
Channel: strconv.FormatInt(wsTickerDataID, 10),
})
if p.IsWebsocketAuthenticationSupported() {
subscriptions = append(subscriptions, stream.ChannelSubscription{
subscriptions = append(subscriptions, subscription.Subscription{
Channel: strconv.FormatInt(wsAccountNotificationID, 10),
})
}
for j := range enabledCurrencies {
enabledCurrencies[j].Delimiter = currency.UnderscoreDelimiter
subscriptions = append(subscriptions, stream.ChannelSubscription{
Channel: "orderbook",
Currency: enabledCurrencies[j],
Asset: asset.Spot,
for j := range enabledPairs {
enabledPairs[j].Delimiter = currency.UnderscoreDelimiter
subscriptions = append(subscriptions, subscription.Subscription{
Channel: "orderbook",
Pair: enabledPairs[j],
Asset: asset.Spot,
})
}
return subscriptions, nil
}
// Subscribe sends a websocket message to receive data from the channel
func (p *Poloniex) Subscribe(sub []stream.ChannelSubscription) error {
func (p *Poloniex) Subscribe(sub []subscription.Subscription) error {
var creds *account.Credentials
if p.IsWebsocketAuthenticationSupported() {
var err error
@@ -598,7 +599,7 @@ channels:
sub[i].Channel):
subscriptionRequest.Channel = wsTickerDataID
default:
subscriptionRequest.Channel = sub[i].Currency.String()
subscriptionRequest.Channel = sub[i].Pair.String()
}
err := p.Websocket.Conn.SendJSONMessage(subscriptionRequest)
@@ -616,7 +617,7 @@ channels:
}
// Unsubscribe sends a websocket message to stop receiving data from the channel
func (p *Poloniex) Unsubscribe(unsub []stream.ChannelSubscription) error {
func (p *Poloniex) Unsubscribe(unsub []subscription.Subscription) error {
var creds *account.Credentials
if p.IsWebsocketAuthenticationSupported() {
var err error
@@ -645,7 +646,7 @@ channels:
unsub[i].Channel):
unsubscriptionRequest.Channel = wsTickerDataID
default:
unsubscriptionRequest.Channel = unsub[i].Currency.String()
unsubscriptionRequest.Channel = unsub[i].Pair.String()
}
err := p.Websocket.Conn.SendJSONMessage(unsubscriptionRequest)
if err != nil {

View File

@@ -18,6 +18,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/portfolio/withdraw"
@@ -267,7 +268,7 @@ func (c *CustomEx) SupportsREST() bool {
}
// GetSubscriptions is a mock method for CustomEx
func (c *CustomEx) GetSubscriptions() ([]stream.ChannelSubscription, error) {
func (c *CustomEx) GetSubscriptions() ([]subscription.Subscription, error) {
return nil, nil
}
@@ -322,12 +323,12 @@ func (c *CustomEx) SupportsWebsocket() bool {
}
// SubscribeToWebsocketChannels is a mock method for CustomEx
func (c *CustomEx) SubscribeToWebsocketChannels(_ []stream.ChannelSubscription) error {
func (c *CustomEx) SubscribeToWebsocketChannels(_ []subscription.Subscription) error {
return nil
}
// UnsubscribeToWebsocketChannels is a mock method for CustomEx
func (c *CustomEx) UnsubscribeToWebsocketChannels(_ []stream.ChannelSubscription) error {
func (c *CustomEx) UnsubscribeToWebsocketChannels(_ []subscription.Subscription) error {
return nil
}

View File

@@ -18,6 +18,7 @@ import (
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/stream"
"github.com/thrasher-corp/gocryptotrader/exchanges/subscription"
)
// This package is only to be referenced in test files
@@ -61,8 +62,8 @@ func NewTestWebsocket() *stream.Websocket {
ToRoutine: make(chan interface{}, 1000),
TrafficAlert: make(chan struct{}),
ReadMessageErrors: make(chan error),
Subscribe: make(chan []stream.ChannelSubscription, 10),
Unsubscribe: make(chan []stream.ChannelSubscription, 10),
Subscribe: make(chan []subscription.Subscription, 10),
Unsubscribe: make(chan []subscription.Subscription, 10),
Match: stream.NewMatch(),
}
}

View File

@@ -31,33 +31,6 @@ type Response struct {
Raw []byte
}
// DefaultChannelKey is the fallback key for AddSuccessfulSubscriptions
type DefaultChannelKey struct {
Channel string
Currency currency.Pair
Asset asset.Item
}
// ChannelState tracks the status of a subscription channel
type ChannelState uint8
const (
ChannelStateUnknown ChannelState = iota // ChannelStateUnknown means subscription state is not registered, but doesn't imply Inactive
ChannelSubscribing // ChannelSubscribing means channel is in the process of subscribing
ChannelSubscribed // ChannelSubscribed means the channel has finished a successful and acknowledged subscription
ChannelUnsubscribing // ChannelUnsubscribing means the channel has started to unsubscribe, but not yet confirmed
)
// ChannelSubscription container for streaming subscription channels
type ChannelSubscription struct {
Key any
Channel string
Currency currency.Pair
Asset asset.Item
Params map[string]interface{}
State ChannelState
}
// ConnectionSetup defines variables for an individual stream connection
type ConnectionSetup struct {
ResponseCheckTimeout time.Duration

View File

@@ -11,6 +11,7 @@ import (
"github.com/gorilla/websocket"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/config"
"github.com/thrasher-corp/gocryptotrader/exchanges/subscription"
"github.com/thrasher-corp/gocryptotrader/log"
)
@@ -30,6 +31,8 @@ var (
ErrSubscribedAlready = errors.New("duplicate subscription")
// ErrSubscriptionFailure defines an error when a subscription fails
ErrSubscriptionFailure = errors.New("subscription failure")
// ErrSubscriptionNotSupported defines an error when a subscription channel is not supported by an exchange
ErrSubscriptionNotSupported = errors.New("subscription channel not supported ")
// ErrUnsubscribeFailure defines an error when a unsubscribe fails
ErrUnsubscribeFailure = errors.New("unsubscribe failure")
// ErrChannelInStateAlready defines an error when a subscription channel is already in a new state
@@ -79,8 +82,8 @@ func New() *Websocket {
ToRoutine: make(chan interface{}, defaultJobBuffer),
TrafficAlert: make(chan struct{}),
ReadMessageErrors: make(chan error),
Subscribe: make(chan []ChannelSubscription),
Unsubscribe: make(chan []ChannelSubscription),
Subscribe: make(chan []subscription.Subscription),
Unsubscribe: make(chan []subscription.Subscription),
Match: NewMatch(),
}
}
@@ -869,9 +872,9 @@ func (w *Websocket) GetName() string {
// GetChannelDifference finds the difference between the subscribed channels
// and the new subscription list when pairs are disabled or enabled.
func (w *Websocket) GetChannelDifference(genSubs []ChannelSubscription) (sub, unsub []ChannelSubscription) {
func (w *Websocket) GetChannelDifference(genSubs []subscription.Subscription) (sub, unsub []subscription.Subscription) {
w.subscriptionMutex.RLock()
unsubMap := make(map[any]ChannelSubscription, len(w.subscriptions))
unsubMap := make(map[any]subscription.Subscription, len(w.subscriptions))
for k, c := range w.subscriptions {
unsubMap[k] = *c
}
@@ -894,7 +897,7 @@ func (w *Websocket) GetChannelDifference(genSubs []ChannelSubscription) (sub, un
}
// UnsubscribeChannels unsubscribes from a websocket channel
func (w *Websocket) UnsubscribeChannels(channels []ChannelSubscription) error {
func (w *Websocket) UnsubscribeChannels(channels []subscription.Subscription) error {
if len(channels) == 0 {
return fmt.Errorf("%s websocket: %w", w.exchangeName, errNoSubscriptionsSupplied)
}
@@ -912,16 +915,16 @@ func (w *Websocket) UnsubscribeChannels(channels []ChannelSubscription) error {
}
// ResubscribeToChannel resubscribes to channel
func (w *Websocket) ResubscribeToChannel(subscribedChannel *ChannelSubscription) error {
err := w.UnsubscribeChannels([]ChannelSubscription{*subscribedChannel})
func (w *Websocket) ResubscribeToChannel(subscribedChannel *subscription.Subscription) error {
err := w.UnsubscribeChannels([]subscription.Subscription{*subscribedChannel})
if err != nil {
return err
}
return w.SubscribeToChannels([]ChannelSubscription{*subscribedChannel})
return w.SubscribeToChannels([]subscription.Subscription{*subscribedChannel})
}
// SubscribeToChannels appends supplied channels to channelsToSubscribe
func (w *Websocket) SubscribeToChannels(channels []ChannelSubscription) error {
func (w *Websocket) SubscribeToChannels(channels []subscription.Subscription) error {
if err := w.checkSubscriptions(channels); err != nil {
return fmt.Errorf("%s websocket: %w", w.exchangeName, common.AppendError(ErrSubscriptionFailure, err))
}
@@ -933,7 +936,7 @@ func (w *Websocket) SubscribeToChannels(channels []ChannelSubscription) error {
// AddSubscription adds a subscription to the subscription lists
// Unlike AddSubscriptions this method will error if the subscription already exists
func (w *Websocket) AddSubscription(c *ChannelSubscription) error {
func (w *Websocket) AddSubscription(c *subscription.Subscription) error {
w.subscriptionMutex.Lock()
defer w.subscriptionMutex.Unlock()
if w.subscriptions == nil {
@@ -952,7 +955,7 @@ func (w *Websocket) AddSubscription(c *ChannelSubscription) error {
// SetSubscriptionState sets an existing subscription state
// returns an error if the subscription is not found, or the new state is already set
func (w *Websocket) SetSubscriptionState(c *ChannelSubscription, state ChannelState) error {
func (w *Websocket) SetSubscriptionState(c *subscription.Subscription, state subscription.State) error {
w.subscriptionMutex.Lock()
defer w.subscriptionMutex.Unlock()
if w.subscriptions == nil {
@@ -966,7 +969,7 @@ func (w *Websocket) SetSubscriptionState(c *ChannelSubscription, state ChannelSt
if state == p.State {
return ErrChannelInStateAlready
}
if state > ChannelUnsubscribing {
if state > subscription.UnsubscribingState {
return errInvalidChannelState
}
p.State = state
@@ -975,7 +978,7 @@ func (w *Websocket) SetSubscriptionState(c *ChannelSubscription, state ChannelSt
// AddSuccessfulSubscriptions adds subscriptions to the subscription lists that
// has been successfully subscribed
func (w *Websocket) AddSuccessfulSubscriptions(channels ...ChannelSubscription) {
func (w *Websocket) AddSuccessfulSubscriptions(channels ...subscription.Subscription) {
w.subscriptionMutex.Lock()
defer w.subscriptionMutex.Unlock()
if w.subscriptions == nil {
@@ -984,13 +987,13 @@ func (w *Websocket) AddSuccessfulSubscriptions(channels ...ChannelSubscription)
for _, cN := range channels {
c := cN // cN is an iteration var; Not safe to make a pointer to
key := c.EnsureKeyed()
c.State = ChannelSubscribed
c.State = subscription.SubscribedState
w.subscriptions[key] = &c
}
}
// RemoveSubscriptions removes subscriptions from the subscription list
func (w *Websocket) RemoveSubscriptions(channels ...ChannelSubscription) {
func (w *Websocket) RemoveSubscriptions(channels ...subscription.Subscription) {
w.subscriptionMutex.Lock()
defer w.subscriptionMutex.Unlock()
if w.subscriptions == nil {
@@ -1002,22 +1005,9 @@ func (w *Websocket) RemoveSubscriptions(channels ...ChannelSubscription) {
}
}
// EnsureKeyed sets the default key on a channel if it doesn't have one
// Returns key for convenience
func (c *ChannelSubscription) EnsureKeyed() any {
if c.Key == nil {
c.Key = DefaultChannelKey{
Channel: c.Channel,
Asset: c.Asset,
Currency: c.Currency,
}
}
return c.Key
}
// GetSubscription returns a pointer to a copy of the subscription at the key provided
// returns nil if no subscription is at that key or the key is nil
func (w *Websocket) GetSubscription(key any) *ChannelSubscription {
func (w *Websocket) GetSubscription(key any) *subscription.Subscription {
if key == nil || w == nil || w.subscriptions == nil {
return nil
}
@@ -1031,10 +1021,10 @@ func (w *Websocket) GetSubscription(key any) *ChannelSubscription {
}
// GetSubscriptions returns a new slice of the subscriptions
func (w *Websocket) GetSubscriptions() []ChannelSubscription {
func (w *Websocket) GetSubscriptions() []subscription.Subscription {
w.subscriptionMutex.RLock()
defer w.subscriptionMutex.RUnlock()
subs := make([]ChannelSubscription, 0, len(w.subscriptions))
subs := make([]subscription.Subscription, 0, len(w.subscriptions))
for _, c := range w.subscriptions {
subs = append(subs, *c)
}
@@ -1082,7 +1072,7 @@ func checkWebsocketURL(s string) error {
// checkSubscriptions checks subscriptions against the max subscription limit
// and if the subscription already exists.
func (w *Websocket) checkSubscriptions(subs []ChannelSubscription) error {
func (w *Websocket) checkSubscriptions(subs []subscription.Subscription) error {
if len(subs) == 0 {
return errNoSubscriptionsSupplied
}

View File

@@ -20,8 +20,8 @@ import (
"github.com/stretchr/testify/assert"
"github.com/thrasher-corp/gocryptotrader/config"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/protocol"
"github.com/thrasher-corp/gocryptotrader/exchanges/subscription"
)
const (
@@ -73,10 +73,10 @@ var defaultSetup = &WebsocketSetup{
DefaultURL: "testDefaultURL",
RunningURL: "wss://testRunningURL",
Connector: func() error { return nil },
Subscriber: func(_ []ChannelSubscription) error { return nil },
Unsubscriber: func(_ []ChannelSubscription) error { return nil },
GenerateSubscriptions: func() ([]ChannelSubscription, error) {
return []ChannelSubscription{
Subscriber: func(_ []subscription.Subscription) error { return nil },
Unsubscriber: func(_ []subscription.Subscription) error { return nil },
GenerateSubscriptions: func() ([]subscription.Subscription, error) {
return []subscription.Subscription{
{Channel: "TestSub"},
{Channel: "TestSub2", Key: "purple"},
{Channel: "TestSub3", Key: testSubKey{"mauve"}},
@@ -156,20 +156,20 @@ func TestSetup(t *testing.T) {
t.Fatalf("received: '%v' but expected: '%v'", err, errWebsocketSubscriberUnset)
}
websocketSetup.Subscriber = func([]ChannelSubscription) error { return nil }
websocketSetup.Subscriber = func([]subscription.Subscription) error { return nil }
websocketSetup.Features.Unsubscribe = true
err = w.Setup(websocketSetup)
if !errors.Is(err, errWebsocketUnsubscriberUnset) {
t.Fatalf("received: '%v' but expected: '%v'", err, errWebsocketUnsubscriberUnset)
}
websocketSetup.Unsubscriber = func([]ChannelSubscription) error { return nil }
websocketSetup.Unsubscriber = func([]subscription.Subscription) error { return nil }
err = w.Setup(websocketSetup)
if !errors.Is(err, errWebsocketSubscriptionsGeneratorUnset) {
t.Fatalf("received: '%v' but expected: '%v'", err, errWebsocketSubscriptionsGeneratorUnset)
}
websocketSetup.GenerateSubscriptions = func() ([]ChannelSubscription, error) { return nil, nil }
websocketSetup.GenerateSubscriptions = func() ([]subscription.Subscription, error) { return nil, nil }
err = w.Setup(websocketSetup)
if !errors.Is(err, errDefaultURLIsEmpty) {
t.Fatalf("received: '%v' but expected: '%v'", err, errDefaultURLIsEmpty)
@@ -504,11 +504,11 @@ func TestSubscribeUnsubscribe(t *testing.T) {
ws := *New()
assert.NoError(t, ws.Setup(defaultSetup), "WS Setup should not error")
fnSub := func(subs []ChannelSubscription) error {
fnSub := func(subs []subscription.Subscription) error {
ws.AddSuccessfulSubscriptions(subs...)
return nil
}
fnUnsub := func(unsubs []ChannelSubscription) error {
fnUnsub := func(unsubs []subscription.Subscription) error {
ws.RemoveSubscriptions(unsubs...)
return nil
}
@@ -522,7 +522,7 @@ func TestSubscribeUnsubscribe(t *testing.T) {
assert.Nil(t, ws.GetSubscription(42), "GetSubscription on empty internal map should return")
assert.NoError(t, ws.SubscribeToChannels(subs), "Basic Subscribing should not error")
assert.Len(t, ws.GetSubscriptions(), 4, "Should have 4 subscriptions")
byDefKey := ws.GetSubscription(DefaultChannelKey{Channel: "TestSub"})
byDefKey := ws.GetSubscription(subscription.DefaultKey{Channel: "TestSub"})
if assert.NotNil(t, byDefKey, "GetSubscription by default key should find a channel") {
assert.Equal(t, "TestSub", byDefKey.Channel, "GetSubscription by default key should return a pointer a copy of the right channel")
assert.NotSame(t, byDefKey, ws.subscriptions["TestSub"], "GetSubscription returns a fresh pointer")
@@ -556,18 +556,18 @@ func TestResubscribe(t *testing.T) {
err = ws.Setup(defaultSetup)
assert.NoError(t, err, "WS Setup should not error")
fnSub := func(subs []ChannelSubscription) error {
fnSub := func(subs []subscription.Subscription) error {
ws.AddSuccessfulSubscriptions(subs...)
return nil
}
fnUnsub := func(unsubs []ChannelSubscription) error {
fnUnsub := func(unsubs []subscription.Subscription) error {
ws.RemoveSubscriptions(unsubs...)
return nil
}
ws.Subscriber = fnSub
ws.Unsubscriber = fnUnsub
channel := []ChannelSubscription{{Channel: "resubTest"}}
channel := []subscription.Subscription{{Channel: "resubTest"}}
assert.ErrorIs(t, ws.ResubscribeToChannel(&channel[0]), ErrSubscriptionNotFound, "Resubscribe should error when channel isn't subscribed yet")
assert.NoError(t, ws.SubscribeToChannels(channel), "Subscribe should not error")
@@ -579,25 +579,25 @@ func TestSubscriptionState(t *testing.T) {
t.Parallel()
ws := New()
c := &ChannelSubscription{Key: 42, Channel: "Gophers", State: ChannelSubscribing}
assert.ErrorIs(t, ws.SetSubscriptionState(c, ChannelUnsubscribing), ErrSubscriptionNotFound, "Setting an imaginary sub should error")
c := &subscription.Subscription{Key: 42, Channel: "Gophers", State: subscription.SubscribingState}
assert.ErrorIs(t, ws.SetSubscriptionState(c, subscription.UnsubscribingState), ErrSubscriptionNotFound, "Setting an imaginary sub should error")
assert.NoError(t, ws.AddSubscription(c), "Adding first subscription should not error")
found := ws.GetSubscription(42)
assert.NotNil(t, found, "Should find the subscription")
assert.Equal(t, ChannelSubscribing, found.State, "Subscription should be Subscribing")
assert.Equal(t, subscription.SubscribingState, found.State, "Subscription should be Subscribing")
assert.ErrorIs(t, ws.AddSubscription(c), ErrSubscribedAlready, "Adding an already existing sub should error")
assert.ErrorIs(t, ws.SetSubscriptionState(c, ChannelSubscribing), ErrChannelInStateAlready, "Setting Same state should error")
assert.ErrorIs(t, ws.SetSubscriptionState(c, ChannelUnsubscribing+1), errInvalidChannelState, "Setting an invalid state should error")
assert.ErrorIs(t, ws.SetSubscriptionState(c, subscription.SubscribingState), ErrChannelInStateAlready, "Setting Same state should error")
assert.ErrorIs(t, ws.SetSubscriptionState(c, subscription.UnsubscribingState+1), errInvalidChannelState, "Setting an invalid state should error")
ws.AddSuccessfulSubscriptions(*c)
found = ws.GetSubscription(42)
assert.NotNil(t, found, "Should find the subscription")
assert.Equal(t, found.State, ChannelSubscribed, "Subscription should be subscribed state")
assert.Equal(t, found.State, subscription.SubscribedState, "Subscription should be subscribed state")
assert.NoError(t, ws.SetSubscriptionState(c, ChannelUnsubscribing), "Setting Unsub state should not error")
assert.NoError(t, ws.SetSubscriptionState(c, subscription.UnsubscribingState), "Setting Unsub state should not error")
found = ws.GetSubscription(42)
assert.Equal(t, found.State, ChannelUnsubscribing, "Subscription should be unsubscribing state")
assert.Equal(t, found.State, subscription.UnsubscribingState, "Subscription should be unsubscribing state")
}
// TestRemoveSubscriptions tests removing a subscription
@@ -605,7 +605,7 @@ func TestRemoveSubscriptions(t *testing.T) {
t.Parallel()
ws := New()
c := &ChannelSubscription{Key: 42, Channel: "Unite!"}
c := &subscription.Subscription{Key: 42, Channel: "Unite!"}
assert.NoError(t, ws.AddSubscription(c), "Adding first subscription should not error")
assert.NotNil(t, ws.GetSubscription(42), "Added subscription should be findable")
@@ -668,35 +668,6 @@ func TestGetSubscriptions(t *testing.T) {
assert.Equal(t, "hello3", w.GetSubscriptions()[0].Channel, "GetSubscriptions should return the correct channel details")
}
// TestEnsureKeyed logic test
func TestEnsureKeyed(t *testing.T) {
t.Parallel()
c := ChannelSubscription{
Channel: "candles",
Asset: asset.Spot,
Currency: currency.NewPair(currency.BTC, currency.USDT),
}
k1, ok := c.EnsureKeyed().(DefaultChannelKey)
if assert.True(t, ok, "EnsureKeyed should return a DefaultChannelKey") {
assert.Exactly(t, k1, c.Key, "EnsureKeyed should set the same key")
assert.Equal(t, k1.Channel, c.Channel, "DefaultChannelKey channel should be correct")
assert.Equal(t, k1.Asset, c.Asset, "DefaultChannelKey asset should be correct")
assert.Equal(t, k1.Currency, c.Currency, "DefaultChannelKey currency should be correct")
}
type platypus string
c = ChannelSubscription{
Key: platypus("Gerald"),
Channel: "orderbook",
Asset: asset.Margin,
Currency: currency.NewPair(currency.ETH, currency.USDC),
}
k2, ok := c.EnsureKeyed().(platypus)
if assert.True(t, ok, "EnsureKeyed should return a platypus") {
assert.Exactly(t, k2, c.Key, "EnsureKeyed should set the same key")
assert.EqualValues(t, "Gerald", k2, "key should have the correct value")
}
}
// TestSetCanUseAuthenticatedEndpoints logic test
func TestSetCanUseAuthenticatedEndpoints(t *testing.T) {
t.Parallel()
@@ -1076,7 +1047,7 @@ func TestGetChannelDifference(t *testing.T) {
t.Parallel()
web := Websocket{}
newChans := []ChannelSubscription{
newChans := []subscription.Subscription{
{
Channel: "Test1",
},
@@ -1093,7 +1064,7 @@ func TestGetChannelDifference(t *testing.T) {
web.AddSuccessfulSubscriptions(subs...)
flushedSubs := []ChannelSubscription{
flushedSubs := []subscription.Subscription{
{
Channel: "Test2",
},
@@ -1103,7 +1074,7 @@ func TestGetChannelDifference(t *testing.T) {
assert.Len(t, subs, 0, "Should get the correct number of subs")
assert.Len(t, unsubs, 2, "Should get the correct number of unsubs")
flushedSubs = []ChannelSubscription{
flushedSubs = []subscription.Subscription{
{
Channel: "Test2",
},
@@ -1126,23 +1097,23 @@ func TestGetChannelDifference(t *testing.T) {
// GenSubs defines a theoretical exchange with pair management
type GenSubs struct {
EnabledPairs currency.Pairs
subscribos []ChannelSubscription
unsubscribos []ChannelSubscription
subscribos []subscription.Subscription
unsubscribos []subscription.Subscription
}
// generateSubs default subs created from the enabled pairs list
func (g *GenSubs) generateSubs() ([]ChannelSubscription, error) {
superduperchannelsubs := make([]ChannelSubscription, len(g.EnabledPairs))
func (g *GenSubs) generateSubs() ([]subscription.Subscription, error) {
superduperchannelsubs := make([]subscription.Subscription, len(g.EnabledPairs))
for i := range g.EnabledPairs {
superduperchannelsubs[i] = ChannelSubscription{
Channel: "TEST:" + strconv.FormatInt(int64(i), 10),
Currency: g.EnabledPairs[i],
superduperchannelsubs[i] = subscription.Subscription{
Channel: "TEST:" + strconv.FormatInt(int64(i), 10),
Pair: g.EnabledPairs[i],
}
}
return superduperchannelsubs, nil
}
func (g *GenSubs) SUBME(subs []ChannelSubscription) error {
func (g *GenSubs) SUBME(subs []subscription.Subscription) error {
if len(subs) == 0 {
return errors.New("WOW")
}
@@ -1150,7 +1121,7 @@ func (g *GenSubs) SUBME(subs []ChannelSubscription) error {
return nil
}
func (g *GenSubs) UNSUBME(unsubs []ChannelSubscription) error {
func (g *GenSubs) UNSUBME(unsubs []subscription.Subscription) error {
if len(unsubs) == 0 {
return errors.New("WOW")
}
@@ -1197,19 +1168,19 @@ func TestFlushChannels(t *testing.T) {
// this to an unconnected state
}
problemFunc := func() ([]ChannelSubscription, error) {
problemFunc := func() ([]subscription.Subscription, error) {
return nil, errors.New("problems")
}
noSub := func() ([]ChannelSubscription, error) {
noSub := func() ([]subscription.Subscription, error) {
return nil, nil
}
// Disable pair and flush system
newgen.EnabledPairs = []currency.Pair{
currency.NewPair(currency.BTC, currency.AUD)}
web.GenerateSubs = func() ([]ChannelSubscription, error) {
return []ChannelSubscription{{Channel: "test"}}, nil
web.GenerateSubs = func() ([]subscription.Subscription, error) {
return []subscription.Subscription{{Channel: "test"}}, nil
}
err = web.FlushChannels()
if err != nil {
@@ -1256,14 +1227,14 @@ func TestFlushChannels(t *testing.T) {
web.subscriptionMutex.Lock()
web.subscriptions = subscriptionMap{
41: {
Key: 41,
Channel: "match channel",
Currency: currency.NewPair(currency.BTC, currency.AUD),
Key: 41,
Channel: "match channel",
Pair: currency.NewPair(currency.BTC, currency.AUD),
},
42: {
Key: 42,
Channel: "unsub channel",
Currency: currency.NewPair(currency.THETA, currency.USDT),
Key: 42,
Channel: "unsub channel",
Pair: currency.NewPair(currency.THETA, currency.USDT),
},
}
web.subscriptionMutex.Unlock()
@@ -1309,10 +1280,10 @@ func TestEnable(t *testing.T) {
connector: connect,
Wg: new(sync.WaitGroup),
ShutdownC: make(chan struct{}),
GenerateSubs: func() ([]ChannelSubscription, error) {
return []ChannelSubscription{{Channel: "test"}}, nil
GenerateSubs: func() ([]subscription.Subscription, error) {
return []subscription.Subscription{{Channel: "test"}}, nil
},
Subscriber: func(cs []ChannelSubscription) error { return nil },
Subscriber: func(cs []subscription.Subscription) error { return nil },
}
err := web.Enable()
@@ -1466,7 +1437,7 @@ func TestCheckSubscriptions(t *testing.T) {
ws.MaxSubscriptionsPerConnection = 1
err = ws.checkSubscriptions([]ChannelSubscription{{}, {}})
err = ws.checkSubscriptions([]subscription.Subscription{{}, {}})
if !errors.Is(err, errSubscriptionsExceedsLimit) {
t.Fatalf("received: %v, but expected: %v", err, errSubscriptionsExceedsLimit)
}
@@ -1474,12 +1445,12 @@ func TestCheckSubscriptions(t *testing.T) {
ws.MaxSubscriptionsPerConnection = 2
ws.subscriptions = subscriptionMap{42: {Key: 42, Channel: "test"}}
err = ws.checkSubscriptions([]ChannelSubscription{{Key: 42, Channel: "test"}})
err = ws.checkSubscriptions([]subscription.Subscription{{Key: 42, Channel: "test"}})
if !errors.Is(err, errChannelAlreadySubscribed) {
t.Fatalf("received: %v, but expected: %v", err, errChannelAlreadySubscribed)
}
err = ws.checkSubscriptions([]ChannelSubscription{{}})
err = ws.checkSubscriptions([]subscription.Subscription{{}})
if !errors.Is(err, nil) {
t.Fatalf("received: %v, but expected: %v", err, nil)
}

View File

@@ -9,6 +9,7 @@ import (
"github.com/thrasher-corp/gocryptotrader/exchanges/fill"
"github.com/thrasher-corp/gocryptotrader/exchanges/protocol"
"github.com/thrasher-corp/gocryptotrader/exchanges/stream/buffer"
"github.com/thrasher-corp/gocryptotrader/exchanges/subscription"
"github.com/thrasher-corp/gocryptotrader/exchanges/trade"
)
@@ -22,7 +23,7 @@ const (
UnhandledMessage = " - Unhandled websocket message: "
)
type subscriptionMap map[any]*ChannelSubscription
type subscriptionMap map[any]*subscription.Subscription
// Websocket defines a return type for websocket connections via the interface
// wrapper for routine processing
@@ -50,18 +51,18 @@ type Websocket struct {
subscriptionMutex sync.RWMutex
subscriptions subscriptionMap
Subscribe chan []ChannelSubscription
Unsubscribe chan []ChannelSubscription
Subscribe chan []subscription.Subscription
Unsubscribe chan []subscription.Subscription
// Subscriber function for package defined websocket subscriber
// functionality
Subscriber func([]ChannelSubscription) error
Subscriber func([]subscription.Subscription) error
// Unsubscriber function for packaged defined websocket unsubscriber
// functionality
Unsubscriber func([]ChannelSubscription) error
Unsubscriber func([]subscription.Subscription) error
// GenerateSubs function for package defined websocket generate
// subscriptions functionality
GenerateSubs func() ([]ChannelSubscription, error)
GenerateSubs func() ([]subscription.Subscription, error)
DataHandler chan interface{}
ToRoutine chan interface{}
@@ -108,9 +109,9 @@ type WebsocketSetup struct {
RunningURL string
RunningURLAuth string
Connector func() error
Subscriber func([]ChannelSubscription) error
Unsubscriber func([]ChannelSubscription) error
GenerateSubscriptions func() ([]ChannelSubscription, error)
Subscriber func([]subscription.Subscription) error
Unsubscriber func([]subscription.Subscription) error
GenerateSubscriptions func() ([]subscription.Subscription, error)
Features *protocol.Features
// Local orderbook buffer config values

View File

@@ -0,0 +1,92 @@
package subscription
import (
"encoding/json"
"fmt"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
)
// DefaultKey is the fallback key for AddSuccessfulSubscriptions
type DefaultKey struct {
Channel string
Pair currency.Pair
Asset asset.Item
}
// State tracks the status of a subscription channel
type State uint8
const (
UnknownState State = iota // UnknownState subscription state is not registered, but doesn't imply Inactive
SubscribingState // SubscribingState means channel is in the process of subscribing
SubscribedState // SubscribedState means the channel has finished a successful and acknowledged subscription
UnsubscribingState // UnsubscribingState means the channel has started to unsubscribe, but not yet confirmed
TickerChannel = "ticker" // TickerChannel Subscription Type
OrderbookChannel = "orderbook" // OrderbookChannel Subscription Type
CandlesChannel = "candles" // CandlesChannel Subscription Type
AllOrdersChannel = "allOrders" // AllOrdersChannel Subscription Type
AllTradesChannel = "allTrades" // AllTradesChannel Subscription Type
MyTradesChannel = "myTrades" // MyTradesChannel Subscription Type
MyOrdersChannel = "myOrders" // MyOrdersChannel Subscription Type
)
// Subscription container for streaming subscriptions
type Subscription struct {
Enabled bool `json:"enabled"`
Key any `json:"-"`
Channel string `json:"channel,omitempty"`
Pair currency.Pair `json:"pair,omitempty"`
Asset asset.Item `json:"asset,omitempty"`
Params map[string]interface{} `json:"params,omitempty"`
State State `json:"-"`
Interval kline.Interval `json:"interval,omitempty"`
Levels int `json:"levels,omitempty"`
Authenticated bool `json:"authenticated,omitempty"`
}
// MarshalJSON generates a JSON representation of a Subscription, specifically for config writing
// The only reason it exists is to avoid having to make Pair a pointer, since that would be generally painful
// If Pair becomes a pointer, this method is redundant and should be removed
func (s *Subscription) MarshalJSON() ([]byte, error) {
// None of the usual type embedding tricks seem to work for not emitting an nil Pair
// The embedded type's Pair always fills the empty value
type MaybePair struct {
Enabled bool `json:"enabled"`
Channel string `json:"channel,omitempty"`
Asset asset.Item `json:"asset,omitempty"`
Params map[string]interface{} `json:"params,omitempty"`
Interval kline.Interval `json:"interval,omitempty"`
Levels int `json:"levels,omitempty"`
Authenticated bool `json:"authenticated,omitempty"`
Pair *currency.Pair `json:"pair,omitempty"`
}
k := MaybePair{s.Enabled, s.Channel, s.Asset, s.Params, s.Interval, s.Levels, s.Authenticated, nil}
if s.Pair != currency.EMPTYPAIR {
k.Pair = &s.Pair
}
return json.Marshal(k)
}
// String implements the Stringer interface for Subscription, giving a human representation of the subscription
func (s *Subscription) String() string {
return fmt.Sprintf("%s %s %s", s.Channel, s.Asset, s.Pair)
}
// EnsureKeyed sets the default key on a channel if it doesn't have one
// Returns key for convenience
func (s *Subscription) EnsureKeyed() any {
if s.Key == nil {
s.Key = DefaultKey{
Channel: s.Channel,
Asset: s.Asset,
Pair: s.Pair,
}
}
return s.Key
}

View File

@@ -0,0 +1,60 @@
package subscription
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/assert"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
)
// TestEnsureKeyed logic test
func TestEnsureKeyed(t *testing.T) {
t.Parallel()
c := Subscription{
Channel: "candles",
Asset: asset.Spot,
Pair: currency.NewPair(currency.BTC, currency.USDT),
}
k1, ok := c.EnsureKeyed().(DefaultKey)
if assert.True(t, ok, "EnsureKeyed should return a DefaultKey") {
assert.Exactly(t, k1, c.Key, "EnsureKeyed should set the same key")
assert.Equal(t, k1.Channel, c.Channel, "DefaultKey channel should be correct")
assert.Equal(t, k1.Asset, c.Asset, "DefaultKey asset should be correct")
assert.Equal(t, k1.Pair, c.Pair, "DefaultKey currency should be correct")
}
type platypus string
c = Subscription{
Key: platypus("Gerald"),
Channel: "orderbook",
Asset: asset.Margin,
Pair: currency.NewPair(currency.ETH, currency.USDC),
}
k2, ok := c.EnsureKeyed().(platypus)
if assert.True(t, ok, "EnsureKeyed should return a platypus") {
assert.Exactly(t, k2, c.Key, "EnsureKeyed should set the same key")
assert.EqualValues(t, "Gerald", k2, "key should have the correct value")
}
}
// TestMarshalling logic test
func TestMarshaling(t *testing.T) {
t.Parallel()
j, err := json.Marshal(&Subscription{Channel: CandlesChannel})
assert.NoError(t, err, "Marshalling should not error")
assert.Equal(t, `{"enabled":false,"channel":"candles"}`, string(j), "Marshalling should be clean and concise")
j, err = json.Marshal(&Subscription{Enabled: true, Channel: OrderbookChannel, Interval: kline.FiveMin, Levels: 4})
assert.NoError(t, err, "Marshalling should not error")
assert.Equal(t, `{"enabled":true,"channel":"orderbook","interval":"5m","levels":4}`, string(j), "Marshalling should be clean and concise")
j, err = json.Marshal(&Subscription{Enabled: true, Channel: OrderbookChannel, Interval: kline.FiveMin, Levels: 4, Pair: currency.NewPair(currency.BTC, currency.USDT)})
assert.NoError(t, err, "Marshalling should not error")
assert.Equal(t, `{"enabled":true,"channel":"orderbook","interval":"5m","levels":4,"pair":"BTCUSDT"}`, string(j), "Marshalling should be clean and concise")
j, err = json.Marshal(&Subscription{Enabled: true, Channel: MyTradesChannel, Authenticated: true})
assert.NoError(t, err, "Marshalling should not error")
assert.Equal(t, `{"enabled":true,"channel":"myTrades","authenticated":true}`, string(j), "Marshalling should be clean and concise")
}

View File

@@ -19,6 +19,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"
)
@@ -273,10 +274,10 @@ func (z *ZB) wsHandleData(respRaw []byte) error {
}
// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions()
func (z *ZB) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, error) {
var subscriptions []stream.ChannelSubscription
func (z *ZB) GenerateDefaultSubscriptions() ([]subscription.Subscription, error) {
var subscriptions []subscription.Subscription
// market configuration is its own channel
subscriptions = append(subscriptions, stream.ChannelSubscription{
subscriptions = append(subscriptions, subscription.Subscription{
Channel: "markets",
})
channels := []string{"%s_ticker", "%s_depth", "%s_trades"}
@@ -288,10 +289,10 @@ func (z *ZB) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, error
for i := range channels {
for j := range enabledCurrencies {
enabledCurrencies[j].Delimiter = ""
subscriptions = append(subscriptions, stream.ChannelSubscription{
Channel: fmt.Sprintf(channels[i], enabledCurrencies[j].Lower().String()),
Currency: enabledCurrencies[j].Lower(),
Asset: asset.Spot,
subscriptions = append(subscriptions, subscription.Subscription{
Channel: fmt.Sprintf(channels[i], enabledCurrencies[j].Lower().String()),
Pair: enabledCurrencies[j].Lower(),
Asset: asset.Spot,
})
}
}
@@ -299,7 +300,7 @@ func (z *ZB) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, error
}
// Subscribe sends a websocket message to receive data from the channel
func (z *ZB) Subscribe(channelsToSubscribe []stream.ChannelSubscription) error {
func (z *ZB) Subscribe(channelsToSubscribe []subscription.Subscription) error {
var errs error
for i := range channelsToSubscribe {
subscriptionRequest := Subscription{