bybit: enable multiconnection handling across websocket endpoints (#1670)

* glorious: whooops

* gk: nits

* Leak issue and edge case

* Websocket: Add SendMessageReturnResponses

* whooooooopsie

* gk: nitssssss

* Update exchanges/stream/stream_match.go

Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>

* Update exchanges/stream/stream_match_test.go

Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>

* linter: appease the linter gods

* gk: nits

* gk: drain brain

* started

* more changes before merge match pr

* gateio: still building out

* gateio: finish spot

* fix up tests in gateio

* Add tests for stream package

* rm unused field

* glorious: nits

* rn files, specifically set function names to asset and offload routing to websocket type.

* linter: fix

* Add futures websocket request support

* gateio: integrate with IBOTExchange (cherry pick my nose)

* linter: fix

* glorious: nits

* add counter and update gateio

* fix collision issue

* Update exchanges/stream/websocket.go

Co-authored-by: Scott <gloriousCode@users.noreply.github.com>

* glorious: nits

* add tests

* linter: fix

* After merge

* Add error connection info

* upgrade to upstream merge

* Fix edge case where it does not reconnect made by an already closed connection

* stream coverage

* glorious: nits

* glorious: nits removed asset error handling in stream package

* linter: fix

* rm block

* Add basic readme

* fix asset enabled flush cycle for multi connection

* spella: fix

* linter: fix

* Add glorious suggestions, fix some race thing

* reinstate name before any routine gets spawned

* stop on error in mock tests

* glorious: nits

* Set correct price

* glorious: nits found in CI build

* Add test for drain, bumped wait times as there seems to be something happening on macos CI builds, used context.WithTimeout because its instant.

* mutex across shutdown and connect for protection

* lint: fix

* test time withoffset, reinstate stop

* fix whoops

* const trafficCheckInterval; rm testmain

* y

* fix lint

* bump time check window

* stream: fix intermittant test failures while testing routines and remove code that is not needed.

* spells

* cant do what I did

* protect race due to routine.

* update testURL

* use mock websocket connection instead of test URL's

* linter: fix

* remove url because its throwing errors on CI builds

* connections drop all the time, don't need to worry about not being able to echo back ws data as it can be easily reviewed _test file side.

* remove another superfluous url thats not really set up for this

* spawn overwatch routine when there is no errors, inline checker instead of waiting for a time period, add sleep inline with echo handler as this is really quick and wanted to ensure that latency is handing correctly

* linter: fixerino uperino

* fix ID bug, why I do this, I don't know.

* glorious: panix

* linter: things

* whoops

* dont need to make consecutive Unix() calls

* websocket: fix potential panic on error and no responses and adding waitForResponses

* bybit: enable multiconnection handling across websocket endpoints

* rm debug lines

* rm json parser and handle in json package instead

* in favour of json package unmarshalling

* fix processing issues with tickers

* linter: fix

* linter: fix again

* * change field name OutboundRequestSignature to WrapperDefinedConnectionSignature for agnostic inbound and outbound connections.
* change method name GetOutboundConnection to GetConnection for agnostic inbound and outbound connections.
* drop outbound field map for improved performance just using a range and field check (less complex as well)
* change field name connections to connectionToWrapper for better clarity

* spells and magic and wands

* merge: fixup

* linter: fix

* spelling: fix

* glorious: nits

* comparable check for signature

* mv err var

* glorious: nits and stuff

* attempt to fix race

* linter: fix

* fix tests

* types/time: strict usage of time type for usage with unix timestamps

* fix tests etc

* glorious: nits

* gk: nits; engine log cleanup

* gk: nits; OCD

* gk: nits; move function change file names

* gk: nits; 🚀

* gk: nits; convert variadic function and message inspection to interface and include a specific function for that handling so as to not need nil on every call

* gk: nits; continued

* gk: engine nits; rm loaded exchange

* gk: nits; drop WebsocketLoginResponse

* stream: Add match method EnsureMatchWithData

* gk: nits; rn Inspect to IsFinal

* gk: nits; rn to MessageFilter

* linter: fix

* gateio: update rate limit definitions (cherry-pick)

* Add test and missing

* Shared REST rate limit definitions with Websocket service, set lookup item to nil for systems that do not require rate limiting; add glorious nit

* integrate rate limits for websocket trading spot

* bybit: split public and private processing to dedicated handler add supporting function and tests

* use correct handler for private inbound connection

* conform to match upstream changes

* standardise names to upstream style

* fix wrapper standards test when sending a auth request through a websocket connection

* whoops

* Update exchanges/gateio/gateio_types.go

Co-authored-by: Scott <gloriousCode@users.noreply.github.com>

* glorious: nits

* linter: fix

* linter: overload

* whoops

* spelling fixes on recent merge

* glorious: nits

* linter: fix?

* glorious: nits

* gk: assert errors touched

* gk: unexport derive functions

* gk: nitssssssss

* fix test

* gk: nitters v1

* gk: http status

* gk/nits: Add getAssetFromFuturesPair

* gk: nits single response when submitting

* gk: new pair with delimiter in tests

* gk: param update slice to slice of pointers

* gk: add asset type in params, includes t.Context() for tests

* linter: fix

* linter: fix

* fix merge whoopsie

* glorious: nits

* gk: nit

* linter: fix

* glorious: nits

* linter/misc: fix and remove meows

* okx: update requestID gen func without func wrapping

* RM: functions not needed

* Update docs/ADD_NEW_EXCHANGE.md

Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>

* gk: nitsssssss

* linter: fix

* Update exchanges/bybit/bybit_test.go

Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>

* Update exchanges/bybit/bybit_test.go

Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>

* gk: nit words

* cranktakular: nits

* linter: fix

* cranktakular: nits and expand coverage

* linter: fix?

* misc fix

* cranktakular: missing nit which I thumbed up but did not do. Sillllllly billlyyyy nilllyyy

* cranktakular: nits

* cranktakular: purge DCP ref/handling and add another TODO

* Update exchanges/bybit/bybit_websocket.go

Co-authored-by: Scott <gloriousCode@users.noreply.github.com>

* glorious: nits

* fix test

* fix alignment issue and rm println

* Update exchanges/bybit/bybit_websocket.go

Co-authored-by: Scott <gloriousCode@users.noreply.github.com>

* Update exchanges/bybit/bybit_websocket.go

Co-authored-by: Scott <gloriousCode@users.noreply.github.com>

* glorious: fix

* Update exchanges/bybit/bybit_websocket.go

Co-authored-by: Adrian Gallagher <adrian.gallagher@thrasher.io>

* Update common/common.go

Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>

* Update common/common_test.go

Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>

* Update exchanges/bybit/bybit_test.go

Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>

* gk: nits

* gk: nit with test

---------

Co-authored-by: Ryan O'Hara-Reid <ryan.oharareid@thrasher.io>
Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>
Co-authored-by: Scott <gloriousCode@users.noreply.github.com>
Co-authored-by: Adrian Gallagher <adrian.gallagher@thrasher.io>
This commit is contained in:
Ryan O'Hara-Reid
2025-08-08 14:22:29 +10:00
committed by GitHub
parent ba92ba3254
commit dcf596c72b
32 changed files with 1475 additions and 939 deletions

View File

@@ -29,7 +29,9 @@ import (
// Exchange implements exchange.IBotExchange and contains additional specific api methods for interacting with Bybit
type Exchange struct {
exchange.Base
account accountTypeHolder
messageIDSeq common.Counter
account accountTypeHolder
}
const (
@@ -76,7 +78,6 @@ var (
errTimeWindowRequired = errors.New("time window is required")
errFrozenPeriodRequired = errors.New("frozen period required")
errQuantityLimitRequired = errors.New("quantity limit required")
errInvalidPushData = errors.New("invalid push data")
errInvalidLeverage = errors.New("leverage can't be zero or less then it")
errInvalidPositionMode = errors.New("position mode is invalid")
errInvalidMode = errors.New("mode can't be empty or missing")

View File

@@ -1,85 +0,0 @@
package bybit
import (
"context"
"net/http"
gws "github.com/gorilla/websocket"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchange/websocket"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"github.com/thrasher-corp/gocryptotrader/exchanges/subscription"
)
// WsInverseConnect connects to inverse websocket feed
func (e *Exchange) WsInverseConnect() error {
ctx := context.TODO()
if !e.Websocket.IsEnabled() || !e.IsEnabled() || !e.IsAssetWebsocketSupported(asset.CoinMarginedFutures) {
return websocket.ErrWebsocketNotEnabled
}
e.Websocket.Conn.SetURL(inversePublic)
var dialer gws.Dialer
err := e.Websocket.Conn.Dial(ctx, &dialer, http.Header{})
if err != nil {
return err
}
e.Websocket.Conn.SetupPingHandler(request.Unset, websocket.PingHandler{
MessageType: gws.TextMessage,
Message: []byte(`{"op": "ping"}`),
Delay: bybitWebsocketTimer,
})
e.Websocket.Wg.Add(1)
go e.wsReadData(ctx, asset.CoinMarginedFutures, e.Websocket.Conn)
return nil
}
// GenerateInverseDefaultSubscriptions generates default subscription
func (e *Exchange) GenerateInverseDefaultSubscriptions() (subscription.List, error) {
var subscriptions subscription.List
channels := []string{chanOrderbook, chanPublicTrade, chanPublicTicker}
pairs, err := e.GetEnabledPairs(asset.CoinMarginedFutures)
if err != nil {
return nil, err
}
for z := range pairs {
for x := range channels {
subscriptions = append(subscriptions,
&subscription.Subscription{
Channel: channels[x],
Pairs: currency.Pairs{pairs[z]},
Asset: asset.CoinMarginedFutures,
})
}
}
return subscriptions, nil
}
// InverseSubscribe sends a subscription message to linear public channels.
func (e *Exchange) InverseSubscribe(channelSubscriptions subscription.List) error {
ctx := context.TODO()
return e.handleInversePayloadSubscription(ctx, "subscribe", channelSubscriptions)
}
// InverseUnsubscribe sends an unsubscription messages through linear public channels.
func (e *Exchange) InverseUnsubscribe(channelSubscriptions subscription.List) error {
ctx := context.TODO()
return e.handleInversePayloadSubscription(ctx, "unsubscribe", channelSubscriptions)
}
func (e *Exchange) handleInversePayloadSubscription(ctx context.Context, operation string, channelSubscriptions subscription.List) error {
payloads, err := e.handleSubscriptions(operation, channelSubscriptions)
if err != nil {
return err
}
for a := range payloads {
// The options connection does not send the subscription request id back with the subscription notification payload
// therefore the code doesn't wait for the response to check whether the subscription is successful or not.
err = e.Websocket.Conn.SendJSONMessage(ctx, request.Unset, payloads[a])
if err != nil {
return err
}
}
return nil
}

View File

@@ -1,103 +0,0 @@
package bybit
import (
"context"
"net/http"
gws "github.com/gorilla/websocket"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchange/websocket"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"github.com/thrasher-corp/gocryptotrader/exchanges/subscription"
)
// WsLinearConnect connects to linear a websocket feed
func (e *Exchange) WsLinearConnect() error {
ctx := context.TODO()
if !e.Websocket.IsEnabled() || !e.IsEnabled() || !e.IsAssetWebsocketSupported(asset.LinearContract) {
return websocket.ErrWebsocketNotEnabled
}
e.Websocket.Conn.SetURL(linearPublic)
var dialer gws.Dialer
err := e.Websocket.Conn.Dial(ctx, &dialer, http.Header{})
if err != nil {
return err
}
e.Websocket.Conn.SetupPingHandler(request.Unset, websocket.PingHandler{
MessageType: gws.TextMessage,
Message: []byte(`{"op": "ping"}`),
Delay: bybitWebsocketTimer,
})
e.Websocket.Wg.Add(1)
go e.wsReadData(ctx, asset.LinearContract, e.Websocket.Conn)
if e.IsWebsocketAuthenticationSupported() {
err = e.WsAuth(ctx)
if err != nil {
e.Websocket.DataHandler <- err
e.Websocket.SetCanUseAuthenticatedEndpoints(false)
}
}
return nil
}
// GenerateLinearDefaultSubscriptions generates default subscription
func (e *Exchange) GenerateLinearDefaultSubscriptions() (subscription.List, error) {
var subscriptions subscription.List
channels := []string{chanOrderbook, chanPublicTrade, chanPublicTicker}
pairs, err := e.GetEnabledPairs(asset.USDTMarginedFutures)
if err != nil {
return nil, err
}
linearPairMap := map[asset.Item]currency.Pairs{
asset.USDTMarginedFutures: pairs,
}
usdcPairs, err := e.GetEnabledPairs(asset.USDCMarginedFutures)
if err != nil {
return nil, err
}
linearPairMap[asset.USDCMarginedFutures] = usdcPairs
pairs = append(pairs, usdcPairs...)
for a := range linearPairMap {
for p := range linearPairMap[a] {
for x := range channels {
subscriptions = append(subscriptions,
&subscription.Subscription{
Channel: channels[x],
Pairs: currency.Pairs{pairs[p]},
Asset: a,
})
}
}
}
return subscriptions, nil
}
// LinearSubscribe sends a subscription message to linear public channels.
func (e *Exchange) LinearSubscribe(channelSubscriptions subscription.List) error {
ctx := context.TODO()
return e.handleLinearPayloadSubscription(ctx, "subscribe", channelSubscriptions)
}
// LinearUnsubscribe sends an unsubscription messages through linear public channels.
func (e *Exchange) LinearUnsubscribe(channelSubscriptions subscription.List) error {
ctx := context.TODO()
return e.handleLinearPayloadSubscription(ctx, "unsubscribe", channelSubscriptions)
}
func (e *Exchange) handleLinearPayloadSubscription(ctx context.Context, operation string, channelSubscriptions subscription.List) error {
payloads, err := e.handleSubscriptions(operation, channelSubscriptions)
if err != nil {
return err
}
for a := range payloads {
// The options connection does not send the subscription request id back with the subscription notification payload
// therefore the code doesn't wait for the response to check whether the subscription is successful or not.
err = e.Websocket.Conn.SendJSONMessage(ctx, request.Unset, payloads[a])
if err != nil {
return err
}
}
return nil
}

View File

@@ -1,92 +0,0 @@
package bybit
import (
"context"
"net/http"
"strconv"
gws "github.com/gorilla/websocket"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/encoding/json"
"github.com/thrasher-corp/gocryptotrader/exchange/websocket"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"github.com/thrasher-corp/gocryptotrader/exchanges/subscription"
)
// WsOptionsConnect connects to options a websocket feed
func (e *Exchange) WsOptionsConnect() error {
ctx := context.TODO()
if !e.Websocket.IsEnabled() || !e.IsEnabled() || !e.IsAssetWebsocketSupported(asset.Options) {
return websocket.ErrWebsocketNotEnabled
}
e.Websocket.Conn.SetURL(optionPublic)
var dialer gws.Dialer
err := e.Websocket.Conn.Dial(ctx, &dialer, http.Header{})
if err != nil {
return err
}
pingMessage := PingMessage{Operation: "ping", RequestID: strconv.FormatInt(e.Websocket.Conn.GenerateMessageID(false), 10)}
pingData, err := json.Marshal(pingMessage)
if err != nil {
return err
}
e.Websocket.Conn.SetupPingHandler(request.Unset, websocket.PingHandler{
MessageType: gws.TextMessage,
Message: pingData,
Delay: bybitWebsocketTimer,
})
e.Websocket.Wg.Add(1)
go e.wsReadData(ctx, asset.Options, e.Websocket.Conn)
return nil
}
// GenerateOptionsDefaultSubscriptions generates default subscription
func (e *Exchange) GenerateOptionsDefaultSubscriptions() (subscription.List, error) {
var subscriptions subscription.List
channels := []string{chanOrderbook, chanPublicTrade, chanPublicTicker}
pairs, err := e.GetEnabledPairs(asset.Options)
if err != nil {
return nil, err
}
for z := range pairs {
for x := range channels {
subscriptions = append(subscriptions,
&subscription.Subscription{
Channel: channels[x],
Pairs: currency.Pairs{pairs[z]},
Asset: asset.Options,
})
}
}
return subscriptions, nil
}
// OptionSubscribe sends a subscription message to options public channels.
func (e *Exchange) OptionSubscribe(channelSubscriptions subscription.List) error {
ctx := context.TODO()
return e.handleOptionsPayloadSubscription(ctx, "subscribe", channelSubscriptions)
}
// OptionUnsubscribe sends an unsubscription messages through options public channels.
func (e *Exchange) OptionUnsubscribe(channelSubscriptions subscription.List) error {
ctx := context.TODO()
return e.handleOptionsPayloadSubscription(ctx, "unsubscribe", channelSubscriptions)
}
func (e *Exchange) handleOptionsPayloadSubscription(ctx context.Context, operation string, channelSubscriptions subscription.List) error {
payloads, err := e.handleSubscriptions(operation, channelSubscriptions)
if err != nil {
return err
}
for a := range payloads {
// The options connection does not send the subscription request id back with the subscription notification payload
// therefore the code doesn't wait for the response to check whether the subscription is successful or not.
err = e.Websocket.Conn.SendJSONMessage(ctx, request.Unset, payloads[a])
if err != nil {
return err
}
}
return nil
}

View File

@@ -1,10 +1,12 @@
package bybit
import (
"bytes"
"context"
"errors"
"fmt"
"maps"
"net/http"
"slices"
"testing"
"time"
@@ -19,18 +21,20 @@ import (
"github.com/thrasher-corp/gocryptotrader/encoding/json"
"github.com/thrasher-corp/gocryptotrader/exchange/websocket"
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/fill"
"github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate"
"github.com/thrasher-corp/gocryptotrader/exchanges/futures"
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/margin"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"github.com/thrasher-corp/gocryptotrader/exchanges/sharedtestvalues"
"github.com/thrasher-corp/gocryptotrader/exchanges/subscription"
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
testexch "github.com/thrasher-corp/gocryptotrader/internal/testing/exchange"
testsubs "github.com/thrasher-corp/gocryptotrader/internal/testing/subscriptions"
testws "github.com/thrasher-corp/gocryptotrader/internal/testing/websocket"
"github.com/thrasher-corp/gocryptotrader/portfolio/withdraw"
"github.com/thrasher-corp/gocryptotrader/types"
)
@@ -1623,78 +1627,78 @@ func TestGetWalletBalance(t *testing.T) {
if mockTests {
require.Len(t, r.List, 1, "GetWalletBalance must return a single list result")
assert.Equal(t, types.Number(0.1997), r.List[0].AccountIMRate, "AccountIMRate should match")
assert.Equal(t, types.Number(0.4996), r.List[0].AccountLTV, "AccountLTV should match")
assert.Equal(t, types.Number(0.0399), r.List[0].AccountMMRate, "AccountMMRate should match")
assert.Equal(t, "UNIFIED", r.List[0].AccountType, "AccountType should match")
assert.Equal(t, types.Number(24616.49915805), r.List[0].TotalAvailableBalance, "TotalAvailableBalance should match")
assert.Equal(t, types.Number(41445.9203332), r.List[0].TotalEquity, "TotalEquity should match")
assert.Equal(t, types.Number(6144.46796478), r.List[0].TotalInitialMargin, "TotalInitialMargin should match")
assert.Equal(t, types.Number(1228.89359295), r.List[0].TotalMaintenanceMargin, "TotalMaintenanceMargin should match")
assert.Equal(t, types.Number(30760.96712284), r.List[0].TotalMarginBalance, "TotalMarginBalance should match")
assert.Equal(t, types.Number(0.0), r.List[0].TotalPerpUPL, "TotalPerpUPL should match")
assert.Equal(t, types.Number(30760.96712284), r.List[0].TotalWalletBalance, "TotalWalletBalance should match")
assert.Equal(t, types.Number(0.1997), r.List[0].AccountIMRate, "AccountIMRate should be correct")
assert.Equal(t, types.Number(0.4996), r.List[0].AccountLTV, "AccountLTV should be correct")
assert.Equal(t, types.Number(0.0399), r.List[0].AccountMMRate, "AccountMMRate should be correct")
assert.Equal(t, "UNIFIED", r.List[0].AccountType, "AccountType should be correct")
assert.Equal(t, types.Number(24616.49915805), r.List[0].TotalAvailableBalance, "TotalAvailableBalance should be correct")
assert.Equal(t, types.Number(41445.9203332), r.List[0].TotalEquity, "TotalEquity should be correct")
assert.Equal(t, types.Number(6144.46796478), r.List[0].TotalInitialMargin, "TotalInitialMargin should be correct")
assert.Equal(t, types.Number(1228.89359295), r.List[0].TotalMaintenanceMargin, "TotalMaintenanceMargin should be correct")
assert.Equal(t, types.Number(30760.96712284), r.List[0].TotalMarginBalance, "TotalMarginBalance should be correct")
assert.Equal(t, types.Number(0.0), r.List[0].TotalPerpUPL, "TotalPerpUPL should be correct")
assert.Equal(t, types.Number(30760.96712284), r.List[0].TotalWalletBalance, "TotalWalletBalance should be correct")
require.Len(t, r.List[0].Coin, 3, "GetWalletBalance must return 3 coins")
for x := range r.List[0].Coin {
switch x {
case 0:
assert.Equal(t, types.Number(0.21976631), r.List[0].Coin[x].AccruedInterest, "AccruedInterest should match")
assert.Equal(t, types.Number(0), r.List[0].Coin[x].AvailableToBorrow, "AvailableToBorrow should match")
assert.Equal(t, types.Number(0), r.List[0].Coin[x].AvailableToWithdraw, "AvailableToWithdraw should match")
assert.Equal(t, types.Number(0), r.List[0].Coin[x].Bonus, "Bonus should match")
assert.Equal(t, types.Number(30723.630216383711792744), r.List[0].Coin[x].BorrowAmount, "BorrowAmount should match")
assert.Equal(t, currency.USDC, r.List[0].Coin[x].Coin, "Coin should match")
assert.Equal(t, types.Number(0.21976631), r.List[0].Coin[x].AccruedInterest, "AccruedInterest should be correct")
assert.Equal(t, types.Number(0), r.List[0].Coin[x].AvailableToBorrow, "AvailableToBorrow should be correct")
assert.Equal(t, types.Number(0), r.List[0].Coin[x].AvailableToWithdraw, "AvailableToWithdraw should be correct")
assert.Equal(t, types.Number(0), r.List[0].Coin[x].Bonus, "Bonus should be correct")
assert.Equal(t, types.Number(30723.630216383711792744), r.List[0].Coin[x].BorrowAmount, "BorrowAmount should be correct")
assert.Equal(t, currency.USDC, r.List[0].Coin[x].Coin, "Coin should be correct")
assert.True(t, r.List[0].Coin[x].CollateralSwitch, "CollateralSwitch should match")
assert.Equal(t, types.Number(0), r.List[0].Coin[x].CumulativeRealisedPNL, "CumulativeRealisedPNL should match")
assert.Equal(t, types.Number(-30723.63021638), r.List[0].Coin[x].Equity, "Equity should match")
assert.Equal(t, types.Number(0), r.List[0].Coin[x].Locked, "Locked should match")
assert.Equal(t, types.Number(0), r.List[0].Coin[x].CumulativeRealisedPNL, "CumulativeRealisedPNL should be correct")
assert.Equal(t, types.Number(-30723.63021638), r.List[0].Coin[x].Equity, "Equity should be correct")
assert.Equal(t, types.Number(0), r.List[0].Coin[x].Locked, "Locked should be correct")
assert.True(t, r.List[0].Coin[x].MarginCollateral, "MarginCollateral should match")
assert.Equal(t, types.Number(0), r.List[0].Coin[x].SpotHedgingQuantity, "SpotHedgingQuantity should match")
assert.Equal(t, types.Number(0), r.List[0].Coin[x].TotalOrderIM, "TotalOrderIM should match")
assert.Equal(t, types.Number(0), r.List[0].Coin[x].TotalPositionIM, "TotalPositionIM should match")
assert.Equal(t, types.Number(0), r.List[0].Coin[x].TotalPositionMM, "TotalPositionMM should match")
assert.Equal(t, types.Number(0), r.List[0].Coin[x].UnrealisedPNL, "UnrealisedPNL should match")
assert.Equal(t, types.Number(-30722.33982391), r.List[0].Coin[x].USDValue, "USDValue should match")
assert.Equal(t, types.Number(-30723.63021638), r.List[0].Coin[x].WalletBalance, "WalletBalance should match")
assert.Equal(t, types.Number(0), r.List[0].Coin[x].SpotHedgingQuantity, "SpotHedgingQuantity should be correct")
assert.Equal(t, types.Number(0), r.List[0].Coin[x].TotalOrderIM, "TotalOrderIM should be correct")
assert.Equal(t, types.Number(0), r.List[0].Coin[x].TotalPositionIM, "TotalPositionIM should be correct")
assert.Equal(t, types.Number(0), r.List[0].Coin[x].TotalPositionMM, "TotalPositionMM should be correct")
assert.Equal(t, types.Number(0), r.List[0].Coin[x].UnrealisedPNL, "UnrealisedPNL should be correct")
assert.Equal(t, types.Number(-30722.33982391), r.List[0].Coin[x].USDValue, "USDValue should be correct")
assert.Equal(t, types.Number(-30723.63021638), r.List[0].Coin[x].WalletBalance, "WalletBalance should be correct")
case 1:
assert.Equal(t, types.Number(0), r.List[0].Coin[x].AccruedInterest, "AccruedInterest should match")
assert.Equal(t, types.Number(0), r.List[0].Coin[x].AvailableToBorrow, "AvailableToBorrow should match")
assert.Equal(t, types.Number(1005.79191187), r.List[0].Coin[x].AvailableToWithdraw, "AvailableToWithdraw should match")
assert.Equal(t, types.Number(0), r.List[0].Coin[x].Bonus, "Bonus should match")
assert.Equal(t, types.Number(0), r.List[0].Coin[x].BorrowAmount, "BorrowAmount should match")
assert.Equal(t, currency.AVAX, r.List[0].Coin[x].Coin, "Coin should match")
assert.Equal(t, types.Number(0), r.List[0].Coin[x].AccruedInterest, "AccruedInterest should be correct")
assert.Equal(t, types.Number(0), r.List[0].Coin[x].AvailableToBorrow, "AvailableToBorrow should be correct")
assert.Equal(t, types.Number(1005.79191187), r.List[0].Coin[x].AvailableToWithdraw, "AvailableToWithdraw should be correct")
assert.Equal(t, types.Number(0), r.List[0].Coin[x].Bonus, "Bonus should be correct")
assert.Equal(t, types.Number(0), r.List[0].Coin[x].BorrowAmount, "BorrowAmount should be correct")
assert.Equal(t, currency.AVAX, r.List[0].Coin[x].Coin, "Coin should be correct")
assert.True(t, r.List[0].Coin[x].CollateralSwitch, "CollateralSwitch should match")
assert.Equal(t, types.Number(0), r.List[0].Coin[x].CumulativeRealisedPNL, "CumulativeRealisedPNL should match")
assert.Equal(t, types.Number(2473.9), r.List[0].Coin[x].Equity, "Equity should match")
assert.Equal(t, types.Number(0), r.List[0].Coin[x].Locked, "Locked should match")
assert.Equal(t, types.Number(0), r.List[0].Coin[x].CumulativeRealisedPNL, "CumulativeRealisedPNL should be correct")
assert.Equal(t, types.Number(2473.9), r.List[0].Coin[x].Equity, "Equity should be correct")
assert.Equal(t, types.Number(0), r.List[0].Coin[x].Locked, "Locked should be correct")
assert.True(t, r.List[0].Coin[x].MarginCollateral, "MarginCollateral should match")
assert.Equal(t, types.Number(0), r.List[0].Coin[x].SpotHedgingQuantity, "SpotHedgingQuantity should match")
assert.Equal(t, types.Number(0), r.List[0].Coin[x].TotalOrderIM, "TotalOrderIM should match")
assert.Equal(t, types.Number(0), r.List[0].Coin[x].TotalPositionIM, "TotalPositionIM should match")
assert.Equal(t, types.Number(0), r.List[0].Coin[x].TotalPositionMM, "TotalPositionMM should match")
assert.Equal(t, types.Number(0), r.List[0].Coin[x].UnrealisedPNL, "UnrealisedPNL should match")
assert.Equal(t, types.Number(71233.0214024), r.List[0].Coin[x].USDValue, "USDValue should match")
assert.Equal(t, types.Number(2473.9), r.List[0].Coin[x].WalletBalance, "WalletBalance should match")
assert.Equal(t, types.Number(0), r.List[0].Coin[x].SpotHedgingQuantity, "SpotHedgingQuantity should be correct")
assert.Equal(t, types.Number(0), r.List[0].Coin[x].TotalOrderIM, "TotalOrderIM should be correct")
assert.Equal(t, types.Number(0), r.List[0].Coin[x].TotalPositionIM, "TotalPositionIM should be correct")
assert.Equal(t, types.Number(0), r.List[0].Coin[x].TotalPositionMM, "TotalPositionMM should be correct")
assert.Equal(t, types.Number(0), r.List[0].Coin[x].UnrealisedPNL, "UnrealisedPNL should be correct")
assert.Equal(t, types.Number(71233.0214024), r.List[0].Coin[x].USDValue, "USDValue should be correct")
assert.Equal(t, types.Number(2473.9), r.List[0].Coin[x].WalletBalance, "WalletBalance should be correct")
case 2:
assert.Equal(t, types.Number(0), r.List[0].Coin[x].AccruedInterest, "AccruedInterest should match")
assert.Equal(t, types.Number(0), r.List[0].Coin[x].AvailableToBorrow, "AvailableToBorrow should match")
assert.Equal(t, types.Number(935.1415), r.List[0].Coin[x].AvailableToWithdraw, "AvailableToWithdraw should match")
assert.Equal(t, types.Number(0), r.List[0].Coin[x].Bonus, "Bonus should match")
assert.Equal(t, types.Number(0), r.List[0].Coin[x].BorrowAmount, "BorrowAmount should match")
assert.Equal(t, currency.USDT, r.List[0].Coin[x].Coin, "Coin should match")
assert.Equal(t, types.Number(0), r.List[0].Coin[x].AccruedInterest, "AccruedInterest should be correct")
assert.Equal(t, types.Number(0), r.List[0].Coin[x].AvailableToBorrow, "AvailableToBorrow should be correct")
assert.Equal(t, types.Number(935.1415), r.List[0].Coin[x].AvailableToWithdraw, "AvailableToWithdraw should be correct")
assert.Equal(t, types.Number(0), r.List[0].Coin[x].Bonus, "Bonus should be correct")
assert.Equal(t, types.Number(0), r.List[0].Coin[x].BorrowAmount, "BorrowAmount should be correct")
assert.Equal(t, currency.USDT, r.List[0].Coin[x].Coin, "Coin should be correct")
assert.True(t, r.List[0].Coin[x].CollateralSwitch, "CollateralSwitch should match")
assert.Equal(t, types.Number(0), r.List[0].Coin[x].CumulativeRealisedPNL, "CumulativeRealisedPNL should match")
assert.Equal(t, types.Number(935.1415), r.List[0].Coin[x].Equity, "Equity should match")
assert.Equal(t, types.Number(0), r.List[0].Coin[x].Locked, "Locked should match")
assert.Equal(t, types.Number(0), r.List[0].Coin[x].CumulativeRealisedPNL, "CumulativeRealisedPNL should be correct")
assert.Equal(t, types.Number(935.1415), r.List[0].Coin[x].Equity, "Equity should be correct")
assert.Equal(t, types.Number(0), r.List[0].Coin[x].Locked, "Locked should be correct")
assert.True(t, r.List[0].Coin[x].MarginCollateral, "MarginCollateral should match")
assert.Equal(t, types.Number(0), r.List[0].Coin[x].SpotHedgingQuantity, "SpotHedgingQuantity should match")
assert.Equal(t, types.Number(0), r.List[0].Coin[x].TotalOrderIM, "TotalOrderIM should match")
assert.Equal(t, types.Number(0), r.List[0].Coin[x].TotalPositionIM, "TotalPositionIM should match")
assert.Equal(t, types.Number(0), r.List[0].Coin[x].TotalPositionMM, "TotalPositionMM should match")
assert.Equal(t, types.Number(0), r.List[0].Coin[x].UnrealisedPNL, "UnrealisedPNL should match")
assert.Equal(t, types.Number(935.23875471), r.List[0].Coin[x].USDValue, "USDValue should match")
assert.Equal(t, types.Number(935.1415), r.List[0].Coin[x].WalletBalance, "WalletBalance should match")
assert.Equal(t, types.Number(0), r.List[0].Coin[x].SpotHedgingQuantity, "SpotHedgingQuantity should be correct")
assert.Equal(t, types.Number(0), r.List[0].Coin[x].TotalOrderIM, "TotalOrderIM should be correct")
assert.Equal(t, types.Number(0), r.List[0].Coin[x].TotalPositionIM, "TotalPositionIM should be correct")
assert.Equal(t, types.Number(0), r.List[0].Coin[x].TotalPositionMM, "TotalPositionMM should be correct")
assert.Equal(t, types.Number(0), r.List[0].Coin[x].UnrealisedPNL, "UnrealisedPNL should be correct")
assert.Equal(t, types.Number(935.23875471), r.List[0].Coin[x].USDValue, "USDValue should be correct")
assert.Equal(t, types.Number(935.1415), r.List[0].Coin[x].WalletBalance, "WalletBalance should be correct")
}
}
}
@@ -2885,22 +2889,22 @@ func TestUpdateAccountInfo(t *testing.T) {
switch x {
case 0:
assert.Equal(t, currency.USDC, r.Accounts[0].Currencies[x].Currency, "Currency should be USDC")
assert.Equal(t, -30723.63021638, r.Accounts[0].Currencies[x].Total, "Total amount should match")
assert.Equal(t, -30723.63021638, r.Accounts[0].Currencies[x].Hold, "Hold amount should match")
assert.Equal(t, 30723.630216383714, r.Accounts[0].Currencies[x].Borrowed, "Borrowed amount should match")
assert.Equal(t, 0.0, r.Accounts[0].Currencies[x].Free, "Free amount should match")
assert.Equal(t, -30723.63021638, r.Accounts[0].Currencies[x].Total, "Total amount should be correct")
assert.Equal(t, -30723.63021638, r.Accounts[0].Currencies[x].Hold, "Hold amount should be correct")
assert.Equal(t, 30723.630216383714, r.Accounts[0].Currencies[x].Borrowed, "Borrowed amount should be correct")
assert.Equal(t, 0.0, r.Accounts[0].Currencies[x].Free, "Free amount should be correct")
case 1:
assert.Equal(t, currency.AVAX, r.Accounts[0].Currencies[x].Currency, "Currency should be AVAX")
assert.Equal(t, 2473.9, r.Accounts[0].Currencies[x].Total, "Total amount should match")
assert.Equal(t, 1468.10808813, r.Accounts[0].Currencies[x].Hold, "Hold amount should match")
assert.Equal(t, 0.0, r.Accounts[0].Currencies[x].Borrowed, "Borrowed amount should match")
assert.Equal(t, 1005.79191187, r.Accounts[0].Currencies[x].Free, "Free amount should match")
assert.Equal(t, 2473.9, r.Accounts[0].Currencies[x].Total, "Total amount should be correct")
assert.Equal(t, 1468.10808813, r.Accounts[0].Currencies[x].Hold, "Hold amount should be correct")
assert.Equal(t, 0.0, r.Accounts[0].Currencies[x].Borrowed, "Borrowed amount should be correct")
assert.Equal(t, 1005.79191187, r.Accounts[0].Currencies[x].Free, "Free amount should be correct")
case 2:
assert.Equal(t, currency.USDT, r.Accounts[0].Currencies[x].Currency, "Currency should be USDT")
assert.Equal(t, 935.1415, r.Accounts[0].Currencies[x].Total, "Total amount should match")
assert.Equal(t, 0.0, r.Accounts[0].Currencies[x].Borrowed, "Borrowed amount should match")
assert.Equal(t, 0.0, r.Accounts[0].Currencies[x].Hold, "Hold amount should match")
assert.Equal(t, 935.1415, r.Accounts[0].Currencies[x].Free, "Free amount should match")
assert.Equal(t, 935.1415, r.Accounts[0].Currencies[x].Total, "Total amount should be correct")
assert.Equal(t, 0.0, r.Accounts[0].Currencies[x].Borrowed, "Borrowed amount should be correct")
assert.Equal(t, 0.0, r.Accounts[0].Currencies[x].Hold, "Hold amount should be correct")
assert.Equal(t, 935.1415, r.Accounts[0].Currencies[x].Free, "Free amount should be correct")
}
}
}
@@ -3015,42 +3019,39 @@ func TestCancelBatchOrders(t *testing.T) {
}
}
type FixtureConnection struct {
dialError error
sendMessageReturnResponseOverride []byte
match websocket.Match
websocket.Connection
}
func (d *FixtureConnection) GenerateMessageID(bool) int64 { return 1337 }
func (d *FixtureConnection) SetupPingHandler(request.EndpointLimit, websocket.PingHandler) {}
func (d *FixtureConnection) Dial(context.Context, *gws.Dialer, http.Header) error { return d.dialError }
func (d *FixtureConnection) SendMessageReturnResponse(context.Context, request.EndpointLimit, any, any) ([]byte, error) {
if d.sendMessageReturnResponseOverride != nil {
return d.sendMessageReturnResponseOverride, nil
}
return []byte(`{"success":true,"ret_msg":"subscribe","conn_id":"5758770c-8152-4545-a84f-dae089e56499","req_id":"1","op":"subscribe"}`), nil
}
func (d *FixtureConnection) SendJSONMessage(context.Context, request.EndpointLimit, any) error {
return nil
}
func (d *FixtureConnection) RequireMatchWithData(signature any, data []byte) error {
return d.match.RequireMatchWithData(signature, data)
}
func TestWsConnect(t *testing.T) {
t.Parallel()
if mockTests {
t.Skip(skippingWebsocketFunctionsForMockTesting)
}
err := e.WsConnect()
if err != nil {
t.Error(err)
}
}
func TestWsLinearConnect(t *testing.T) {
t.Parallel()
if mockTests {
t.Skip(skippingWebsocketFunctionsForMockTesting)
}
err := e.WsLinearConnect()
assert.Truef(t, errors.Is(err, websocket.ErrWebsocketNotEnabled) || err == nil, "WsLinerConnect should not error: %s", err)
}
func TestWsInverseConnect(t *testing.T) {
t.Parallel()
if mockTests {
t.Skip(skippingWebsocketFunctionsForMockTesting)
}
err := e.WsInverseConnect()
assert.Truef(t, errors.Is(err, websocket.ErrWebsocketNotEnabled) || err == nil, "WsInverseConnect should not error: %s", err)
}
func TestWsOptionsConnect(t *testing.T) {
t.Parallel()
if mockTests {
t.Skip(skippingWebsocketFunctionsForMockTesting)
}
err := e.WsOptionsConnect()
assert.Truef(t, errors.Is(err, websocket.ErrWebsocketNotEnabled) || err == nil, "WsOptionsConnect should not error: %s", err)
err := e.WsConnect(t.Context(), &FixtureConnection{dialError: nil})
require.NoError(t, err)
exp := errors.New("dial error")
err = e.WsConnect(t.Context(), &FixtureConnection{dialError: exp})
require.ErrorIs(t, err, exp)
}
var pushDataMap = map[string]string{
@@ -3062,22 +3063,195 @@ var pushDataMap = map[string]string{
"Public LT Kline": `{ "type": "snapshot", "topic": "kline_lt.5.BTCUSDT", "data": [ { "start": 1672325100000, "end": 1672325399999, "interval": "5", "open": "0.416039541212402799", "close": "0.41477848043290448", "high": "0.416039541212402799", "low": "0.409734237314911206", "confirm": false, "timestamp": 1672325322393} ], "ts": 1672325322393}`,
"Public LT Ticker": `{ "topic": "tickers_lt.BTCUSDT", "ts": 1672325446847, "type": "snapshot", "data": { "symbol": "BTCUSDT", "lastPrice": "0.41477848043290448", "highPrice24h": "0.435285472510871305", "lowPrice24h": "0.394601507960931382", "prevPrice24h": "0.431502290172376349", "price24hPcnt": "-0.0388" } }`,
"Public LT Navigation": `{ "topic": "lt.EOS3LUSDT", "ts": 1672325564669, "type": "snapshot", "data": { "symbol": "BTCUSDT", "time": 1672325564554, "nav": "0.413517419653406162", "basketPosition": "1.261060779498318641", "leverage": "2.656197506416192150", "basketLoan": "-0.684866519289629374", "circulation": "72767.309468460367138199", "basket": "91764.000000292013277472" } }`,
"Private Position": `{"id": "59232430b58efe-5fc5-4470-9337-4ce293b68edd", "topic": "position", "creationTime": 1672364174455, "data": [ { "positionIdx": 0, "tradeMode": 0, "riskId": 41, "riskLimitValue": "200000", "symbol": "XRPUSDT", "side": "Buy", "size": "75", "entryPrice": "0.3615", "leverage": "10", "positionValue": "27.1125", "positionBalance": "0", "markPrice": "0.3374", "positionIM": "2.72589075", "positionMM": "0.28576575", "takeProfit": "0", "stopLoss": "0", "trailingStop": "0", "unrealisedPnl": "-1.8075", "cumRealisedPnl": "0.64782276", "createdTime": "1672121182216", "updatedTime": "1672364174449", "tpslMode": "Full", "liqPrice": "", "bustPrice": "", "category": "linear","positionStatus":"Normal","adlRankIndicator":2}]}`,
"Private Order": `{ "id": "5923240c6880ab-c59f-420b-9adb-3639adc9dd90", "topic": "order", "creationTime": 1672364262474, "data": [ { "symbol": "BTCUSDT", "orderId": "5cf98598-39a7-459e-97bf-76ca765ee020", "side": "Sell", "orderType": "Market", "cancelType": "UNKNOWN", "price": "72.5", "qty": "1", "orderIv": "", "timeInForce": "IOC", "orderStatus": "Filled", "orderLinkId": "", "lastPriceOnCreated": "", "reduceOnly": false, "leavesQty": "", "leavesValue": "", "cumExecQty": "1", "cumExecValue": "75", "avgPrice": "75", "blockTradeId": "", "positionIdx": 0, "cumExecFee": "0.358635", "createdTime": "1672364262444", "updatedTime": "1672364262457", "rejectReason": "EC_NoError", "stopOrderType": "", "tpslMode": "", "triggerPrice": "", "takeProfit": "", "stopLoss": "", "tpTriggerBy": "", "slTriggerBy": "", "tpLimitPrice": "", "slLimitPrice": "", "triggerDirection": 0, "triggerBy": "", "closeOnTrigger": false, "category": "option", "placeType": "price", "smpType": "None", "smpGroup": 0, "smpOrderId": "" } ] }`,
"Private Wallet": `{ "id": "5923242c464be9-25ca-483d-a743-c60101fc656f", "topic": "wallet", "creationTime": 1672364262482, "data": [ { "accountIMRate": "0.016", "accountMMRate": "0.003", "totalEquity": "12837.78330098", "totalWalletBalance": "12840.4045924", "totalMarginBalance": "12837.78330188", "totalAvailableBalance": "12632.05767702", "totalPerpUPL": "-2.62129051", "totalInitialMargin": "205.72562486", "totalMaintenanceMargin": "39.42876721", "coin": [ { "coin": "USDC", "equity": "200.62572554", "usdValue": "200.62572554", "walletBalance": "201.34882644", "availableToWithdraw": "0", "availableToBorrow": "1500000", "borrowAmount": "0", "accruedInterest": "0", "totalOrderIM": "0", "totalPositionIM": "202.99874213", "totalPositionMM": "39.14289747", "unrealisedPnl": "74.2768991", "cumRealisedPnl": "-209.1544627", "bonus": "0" }, { "coin": "BTC", "equity": "0.06488393", "usdValue": "1023.08402268", "walletBalance": "0.06488393", "availableToWithdraw": "0.06488393", "availableToBorrow": "2.5", "borrowAmount": "0", "accruedInterest": "0", "totalOrderIM": "0", "totalPositionIM": "0", "totalPositionMM": "0", "unrealisedPnl": "0", "cumRealisedPnl": "0", "bonus": "0" }, { "coin": "ETH", "equity": "0", "usdValue": "0", "walletBalance": "0", "availableToWithdraw": "0", "availableToBorrow": "26", "borrowAmount": "0", "accruedInterest": "0", "totalOrderIM": "0", "totalPositionIM": "0", "totalPositionMM": "0", "unrealisedPnl": "0", "cumRealisedPnl": "0", "bonus": "0" }, { "coin": "USDT", "equity": "11726.64664904", "usdValue": "11613.58597018", "walletBalance": "11728.54414904", "availableToWithdraw": "11723.92075829", "availableToBorrow": "2500000", "borrowAmount": "0", "accruedInterest": "0", "totalOrderIM": "0", "totalPositionIM": "2.72589075", "totalPositionMM": "0.28576575", "unrealisedPnl": "-1.8975", "cumRealisedPnl": "0.64782276", "bonus": "0" }, { "coin": "EOS3L", "equity": "215.0570412", "usdValue": "0", "walletBalance": "215.0570412", "availableToWithdraw": "215.0570412", "availableToBorrow": "0", "borrowAmount": "0", "accruedInterest": "", "totalOrderIM": "0", "totalPositionIM": "0", "totalPositionMM": "0", "unrealisedPnl": "0", "cumRealisedPnl": "0", "bonus": "0" }, { "coin": "BIT", "equity": "1.82", "usdValue": "0.48758257", "walletBalance": "1.82", "availableToWithdraw": "1.82", "availableToBorrow": "0", "borrowAmount": "0", "accruedInterest": "", "totalOrderIM": "0", "totalPositionIM": "0", "totalPositionMM": "0", "unrealisedPnl": "0", "cumRealisedPnl": "0", "bonus": "0" } ], "accountType": "UNIFIED", "accountLTV": "0.017" } ] }`,
"Private Greek": `{ "id": "592324fa945a30-2603-49a5-b865-21668c29f2a6", "topic": "greeks", "creationTime": 1672364262482, "data": [ { "baseCoin": "ETH", "totalDelta": "0.06999986", "totalGamma": "-0.00000001", "totalVega": "-0.00000024", "totalTheta": "0.00001314" } ] }`,
"Execution": `{"id": "592324803b2785-26fa-4214-9963-bdd4727f07be", "topic": "execution", "creationTime": 1672364174455, "data": [ { "category": "linear", "symbol": "XRPUSDT", "execFee": "0.005061", "execId": "7e2ae69c-4edf-5800-a352-893d52b446aa", "execPrice": "0.3374", "execQty": "25", "execType": "Trade", "execValue": "8.435", "isMaker": false, "feeRate": "0.0006", "tradeIv": "", "markIv": "", "blockTradeId": "", "markPrice": "0.3391", "indexPrice": "", "underlyingPrice": "", "leavesQty": "0", "orderId": "f6e324ff-99c2-4e89-9739-3086e47f9381", "orderLinkId": "", "orderPrice": "0.3207", "orderQty":"25","orderType":"Market","stopOrderType":"UNKNOWN","side":"Sell","execTime":"1672364174443","isLeverage": "0","closedSize": "","seq":4688002127}]}`,
"pong": `{"op":"pong","args":["1753340040127"],"conn_id":"d157a7favkf4mm3ibuvg-14toog"}`,
"unhandled": `{"topic": "unhandled"}`,
}
func TestPushData(t *testing.T) {
func TestPushDataPublic(t *testing.T) {
t.Parallel()
keys := slices.Collect(maps.Keys(pushDataMap))
slices.Sort(keys)
for x := range keys {
err := e.wsHandleData(t.Context(), asset.Spot, []byte(pushDataMap[keys[x]]))
assert.NoError(t, err, "wsHandleData should not error")
err := e.wsHandleData(nil, asset.Spot, []byte(pushDataMap[keys[x]]))
if keys[x] == "unhandled" {
assert.ErrorIs(t, err, errUnhandledStreamData, "wsHandleData should error correctly for unhandled topics")
} else {
assert.NoError(t, err, "wsHandleData should not error")
}
}
}
func TestWSHandleAuthenticatedData(t *testing.T) {
t.Parallel()
err := e.wsHandleAuthenticatedData(t.Context(), nil, []byte(`{"op":"pong","args":["1753340040127"],"conn_id":"d157a7favkf4mm3ibuvg-14toog"}`))
require.NoError(t, err, "wsHandleAuthenticatedData must not error for pong message")
err = e.wsHandleAuthenticatedData(t.Context(), nil, []byte(`{"topic": "unhandled"}`))
require.ErrorIs(t, err, errUnhandledStreamData, "wsHandleAuthenticatedData must error for unhandled stream data")
e := new(Exchange) //nolint:govet // Intentional shadow
require.NoError(t, testexch.Setup(e), "Test instance Setup must not error")
e.API.AuthenticatedSupport = true
e.API.AuthenticatedWebsocketSupport = true
e.SetCredentials("test", "test", "", "", "", "")
testexch.FixtureToDataHandler(t, "testdata/wsAuth.json", func(ctx context.Context, r []byte) error {
if bytes.Contains(r, []byte("%s")) {
r = fmt.Appendf(nil, string(r), optionsTradablePair.String())
}
return e.wsHandleAuthenticatedData(ctx, nil, r)
})
close(e.Websocket.DataHandler)
require.Len(t, e.Websocket.DataHandler, 6, "Should see correct number of messages")
i := 0
for data := range e.Websocket.DataHandler {
i++
switch v := data.(type) {
case WsPositions:
require.Len(t, v, 1, "must see 1 position")
assert.Zero(t, v[0].PositionIdx, "PositionIdx should be 0")
assert.Zero(t, v[0].TradeMode, "TradeMode should be 0")
assert.Equal(t, int64(41), v[0].RiskID, "RiskID should be correct")
assert.Equal(t, 200000.0, v[0].RiskLimitValue.Float64(), "RiskLimitValue should be correct")
assert.Equal(t, "XRPUSDT", v[0].Symbol, "Symbol should be correct")
assert.Equal(t, "Buy", v[0].Side, "Side should be correct")
assert.Equal(t, 75.0, v[0].Size.Float64(), "Size should be correct")
assert.Equal(t, 0.3615, v[0].EntryPrice.Float64(), "Entry price should be correct")
assert.Equal(t, 10.0, v[0].Leverage.Float64(), "Leverage should be correct")
assert.Equal(t, 27.1125, v[0].PositionValue.Float64(), "Position value should be correct")
assert.Zero(t, v[0].PositionBalance.Float64(), "Position balance should be 0")
assert.Equal(t, 0.3374, v[0].MarkPrice.Float64(), "Mark price should be correct")
assert.Equal(t, 2.72589075, v[0].PositionIM.Float64(), "Position IM should be correct")
assert.Equal(t, 0.28576575, v[0].PositionMM.Float64(), "Position MM should be correct")
assert.Zero(t, v[0].TakeProfit.Float64(), "Take profit should be 0")
assert.Zero(t, v[0].StopLoss.Float64(), "Stop loss should be 0")
assert.Zero(t, v[0].TrailingStop.Float64(), "Trailing stop should be 0")
assert.Equal(t, -1.8075, v[0].UnrealisedPnl.Float64(), "Unrealised PnL should be correct")
assert.Equal(t, 0.64782276, v[0].CumRealisedPnl.Float64(), "Cum realised PnL should be correct")
assert.Equal(t, time.UnixMilli(1672121182216), v[0].CreatedTime.Time(), "Creation time should be correct")
assert.Equal(t, time.UnixMilli(1672364174449), v[0].UpdatedTime.Time(), "Updated time should be correct")
assert.Equal(t, "Full", v[0].TpslMode, "TPSL mode should be correct")
assert.Zero(t, v[0].LiqPrice.Float64(), "Liq price should be 0")
assert.Zero(t, v[0].BustPrice.Float64(), "Bust price should be 0")
assert.Equal(t, "linear", v[0].Category, "Category should be correct")
assert.Equal(t, "Normal", v[0].PositionStatus, "Position status should be correct")
assert.Equal(t, int64(2), v[0].AdlRankIndicator, "ADL Rank Indicator should be correct")
case []order.Detail:
if i == 6 {
require.Len(t, v, 1)
assert.Equal(t, "c1956690-b731-4191-97c0-94b00422231b", v[0].OrderID)
assert.Equal(t, "BTC_USDT", v[0].Pair.String())
assert.Equal(t, order.Sell, v[0].Side)
assert.Equal(t, order.Filled, v[0].Status)
assert.Equal(t, 1.7, v[0].Amount)
assert.Equal(t, 4.033, v[0].Price)
assert.Equal(t, 4.24, v[0].AverageExecutedPrice)
assert.Equal(t, 0.0, v[0].RemainingAmount)
assert.Equal(t, asset.USDTMarginedFutures, v[0].AssetType)
continue
}
require.Len(t, v, 1, "must see 1 order")
assert.True(t, optionsTradablePair.Equal(v[0].Pair), "Pair should match")
assert.Equal(t, "5cf98598-39a7-459e-97bf-76ca765ee020", v[0].OrderID, "Order ID should be correct")
assert.Equal(t, order.Sell, v[0].Side, "Side should be correct")
assert.Equal(t, order.Market, v[0].Type, "Order type should be correct")
assert.Equal(t, 72.5, v[0].Price, "Price should be correct")
assert.Equal(t, 1.0, v[0].Amount, "Amount should be correct")
assert.Equal(t, order.ImmediateOrCancel, v[0].TimeInForce, "Time in force should be correct")
assert.Equal(t, order.Filled, v[0].Status, "Order status should be correct")
assert.Empty(t, v[0].ClientOrderID, "client order ID should be empty")
assert.False(t, v[0].ReduceOnly, "Reduce only should be false")
assert.Equal(t, 1.0, v[0].ExecutedAmount, "executed amount should be correct")
assert.Equal(t, 75.0, v[0].AverageExecutedPrice, "Avg price should be correct")
assert.Equal(t, 0.358635, v[0].Fee, "fee should be correct")
assert.Equal(t, time.UnixMilli(1672364262444), v[0].Date, "Created time should be correct")
assert.Equal(t, time.UnixMilli(1672364262457), v[0].LastUpdated, "Updated time should be correct")
case []account.Change:
require.Len(t, v, 6, "must see 6 items")
for i, change := range v {
assert.Empty(t, change.Account, "Account type should be empty")
assert.Equal(t, asset.Spot, change.AssetType, "Asset type should be Spot")
require.NotNil(t, change.Balance, "balance must not be nil")
switch i {
case 0:
assert.True(t, currency.USDC.Equal(change.Balance.Currency), "currency should match")
assert.Zero(t, change.Balance.AvailableWithoutBorrow, "AvailableWithoutBorrow should zero")
assert.Zero(t, change.Balance.Borrowed, "Borrowed should be 0")
assert.Equal(t, 201.34882644, change.Balance.Free, "Free should be correct")
assert.Zero(t, change.Balance.Hold, "Hold should be 0")
assert.Equal(t, 201.34882644, change.Balance.Total, "Total should be correct")
assert.Equal(t, time.UnixMilli(1672364262482), change.Balance.UpdatedAt, "Last updated should be correct")
case 1:
assert.True(t, currency.BTC.Equal(change.Balance.Currency), "currency should match")
assert.Equal(t, 0.06488393, change.Balance.Free, "Free should be correct")
assert.Zero(t, change.Balance.AvailableWithoutBorrow, "AvailableWithoutBorrow should zero")
assert.Zero(t, change.Balance.Borrowed, "Borrowed should be 0")
assert.Zero(t, change.Balance.Hold, "Hold should be 0")
assert.Equal(t, 0.06488393, change.Balance.Total, "Total should be correct")
assert.Equal(t, time.UnixMilli(1672364262482), change.Balance.UpdatedAt, "Last updated should be correct")
case 2:
assert.True(t, currency.ETH.Equal(change.Balance.Currency), "currency should match")
assert.Zero(t, change.Balance.Free, "Free should be 0")
assert.Zero(t, change.Balance.AvailableWithoutBorrow, "AvailableWithoutBorrow should zero")
assert.Zero(t, change.Balance.Borrowed, "Borrowed should be 0")
assert.Zero(t, change.Balance.Hold, "Hold should be 0")
assert.Zero(t, change.Balance.Total, "Total should be 0")
assert.Equal(t, time.UnixMilli(1672364262482), change.Balance.UpdatedAt, "Last updated should be correct")
case 3:
assert.True(t, currency.USDT.Equal(change.Balance.Currency), "currency should match")
assert.Equal(t, 11728.54414904, change.Balance.Free, "Free should be correct")
assert.Zero(t, change.Balance.AvailableWithoutBorrow, "AvailableWithoutBorrow should be 0")
assert.Zero(t, change.Balance.Borrowed, "Borrowed should be 0")
assert.Zero(t, change.Balance.Hold, "Hold should be 0")
assert.Equal(t, 11728.54414904, change.Balance.Total, "Total should be correct")
assert.Equal(t, time.UnixMilli(1672364262482), change.Balance.UpdatedAt, "Last updated should be correct")
case 4:
assert.True(t, currency.NewCode("EOS3L").Equal(change.Balance.Currency), "currency should match")
assert.Equal(t, 215.0570412, change.Balance.Free, "Free should be correct")
assert.Zero(t, change.Balance.AvailableWithoutBorrow, "AvailableWithoutBorrow should be 0")
assert.Zero(t, change.Balance.Borrowed, "Borrowed should be 0")
assert.Zero(t, change.Balance.Hold, "Hold should be 0")
assert.Equal(t, 215.0570412, change.Balance.Total, "Total should be correct")
assert.Equal(t, time.UnixMilli(1672364262482), change.Balance.UpdatedAt, "Last updated should be correct")
case 5:
assert.True(t, currency.BIT.Equal(change.Balance.Currency), "currency should match")
assert.Equal(t, 1.82, change.Balance.Free, "Free should be correct")
assert.Zero(t, change.Balance.AvailableWithoutBorrow, "AvailableWithoutBorrow should be 0")
assert.Zero(t, change.Balance.Borrowed, "Borrowed should be 0")
assert.Zero(t, change.Balance.Hold, "Hold should be 0")
assert.Equal(t, 1.82, change.Balance.Total, "Total should be correct")
assert.Equal(t, time.UnixMilli(1672364262482), change.Balance.UpdatedAt, "Last updated should be correct")
}
}
case *GreeksResponse:
assert.Equal(t, "592324fa945a30-2603-49a5-b865-21668c29f2a6", v.ID, "ID should be correct")
assert.Equal(t, "greeks", v.Topic, "Topic should be correct")
assert.Equal(t, time.UnixMilli(1672364262482), v.CreationTime.Time(), "Creation time should be correct")
require.Len(t, v.Data, 1, "must see 1 greek")
assert.Equal(t, "ETH", v.Data[0].BaseCoin.String(), "Base coin should be correct")
assert.Equal(t, 0.06999986, v.Data[0].TotalDelta.Float64(), "Total delta should be correct")
assert.Equal(t, -0.00000001, v.Data[0].TotalGamma.Float64(), "Total gamma should be correct")
assert.Equal(t, -0.00000024, v.Data[0].TotalVega.Float64(), "Total vega should be correct")
assert.Equal(t, 0.00001314, v.Data[0].TotalTheta.Float64(), "Total theta should be correct")
case []fill.Data:
require.Len(t, v, 1, "must see 1 fill")
assert.Equal(t, "7e2ae69c-4edf-5800-a352-893d52b446aa", v[0].ID, "ID should be correct")
assert.Equal(t, time.UnixMilli(1672364174443), v[0].Timestamp, "time should be correct")
assert.Equal(t, e.Name, v[0].Exchange, "Exchange name should be correct")
assert.Equal(t, asset.USDTMarginedFutures, v[0].AssetType, "Asset type should be correct")
assert.Equal(t, "XRP_USDT", v[0].CurrencyPair.String(), "Symbol should be correct")
assert.Equal(t, order.Sell, v[0].Side, "Side should be correct")
assert.Equal(t, "f6e324ff-99c2-4e89-9739-3086e47f9381", v[0].OrderID, "Order ID should be correct")
assert.Empty(t, v[0].ClientOrderID, "Client order ID should be empty")
assert.Empty(t, v[0].TradeID, "Trade ID should be empty")
assert.Equal(t, 0.3374, v[0].Price, "price should be correct")
assert.Equal(t, 25.0, v[0].Amount, "amount should be correct")
default:
t.Errorf("Unexpected data received: %v", v)
}
}
}
@@ -3091,7 +3265,7 @@ func TestWsTicker(t *testing.T) {
require.NoError(t, testexch.Setup(e), "Test instance Setup must not error")
testexch.FixtureToDataHandler(t, "testdata/wsTicker.json", func(_ context.Context, r []byte) error {
defer slices.Delete(assetRouting, 0, 1)
return e.wsHandleData(t.Context(), assetRouting[0], r)
return e.wsHandleData(nil, assetRouting[0], r)
})
close(e.Websocket.DataHandler)
expected := 8
@@ -3340,20 +3514,14 @@ func TestFetchTradablePairs(t *testing.T) {
func TestDeltaUpdateOrderbook(t *testing.T) {
t.Parallel()
data := []byte(`{"topic":"orderbook.50.WEMIXUSDT","ts":1697573183768,"type":"snapshot","data":{"s":"WEMIXUSDT","b":[["0.9511","260.703"],["0.9677","0"]],"a":[],"u":3119516,"seq":14126848493},"cts":1728966699481}`)
err := e.wsHandleData(t.Context(), asset.Spot, data)
if err != nil {
t.Fatal(err)
}
err := e.wsHandleData(nil, asset.Spot, data)
require.NoError(t, err, "wsHandleData must not error")
update := []byte(`{"topic":"orderbook.50.WEMIXUSDT","ts":1697573183768,"type":"delta","data":{"s":"WEMIXUSDT","b":[["0.9511","260.703"],["0.9677","0"]],"a":[],"u":3119516,"seq":14126848493},"cts":1728966699481}`)
var wsResponse WebsocketResponse
err = json.Unmarshal(update, &wsResponse)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err, "Unmarshal must not error")
err = e.wsProcessOrderbook(asset.Spot, &wsResponse)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err, "wsProcessOrderbook must not error")
}
func TestGetLongShortRatio(t *testing.T) {
@@ -3577,7 +3745,7 @@ func TestGetCurrencyTradeURL(t *testing.T) {
func TestGenerateSubscriptions(t *testing.T) {
t.Parallel()
e := new(Exchange)
e := new(Exchange) //nolint:govet // Intentional shadow
require.NoError(t, testexch.Setup(e), "Test instance Setup must not error")
e.Websocket.SetCanUseAuthenticatedEndpoints(true)
@@ -3611,11 +3779,6 @@ func TestGenerateSubscriptions(t *testing.T) {
} else {
s.Pairs = pairs
s.QualifiedChannel = channelName(s)
categoryName := getCategoryName(a)
if isCategorisedChannel(s.QualifiedChannel) && categoryName != "" {
s.QualifiedChannel += "." + categoryName
}
exp = append(exp, s)
}
}
@@ -3623,48 +3786,43 @@ func TestGenerateSubscriptions(t *testing.T) {
testsubs.EqualLists(t, exp, subs)
}
func TestSubscribe(t *testing.T) {
t.Parallel()
e := new(Exchange)
require.NoError(t, testexch.Setup(e), "Test instance Setup must not error")
subs, err := e.Features.Subscriptions.ExpandTemplates(e)
require.NoError(t, err, "ExpandTemplates must not error")
e.Features.Subscriptions = subscription.List{}
testexch.SetupWs(t, e)
err = e.Subscribe(subs)
require.NoError(t, err, "Subscribe must not error")
}
func TestAuthSubscribe(t *testing.T) {
t.Parallel()
e := new(Exchange)
e := new(Exchange) //nolint:govet // Intentional shadow
require.NoError(t, testexch.Setup(e), "Test instance Setup must not error")
require.NoError(t, e.authSubscribe(t.Context(), &FixtureConnection{}, subscription.List{}))
authsubs, err := e.generateAuthSubscriptions()
require.NoError(t, err, "generateAuthSubscriptions must not error")
require.Empty(t, authsubs, "generateAuthSubscriptions must not return subs")
e.Websocket.SetCanUseAuthenticatedEndpoints(true)
subs, err := e.Features.Subscriptions.ExpandTemplates(e)
require.NoError(t, err, "ExpandTemplates must not error")
e.Features.Subscriptions = subscription.List{}
success := true
mock := func(tb testing.TB, msg []byte, w *gws.Conn) error {
tb.Helper()
var req SubscriptionArgument
require.NoError(tb, json.Unmarshal(msg, &req), "Unmarshal must not error")
require.Equal(tb, "subscribe", req.Operation)
msg, err = json.Marshal(SubscriptionResponse{
Success: success,
RetMsg: "Mock Resp Error",
RequestID: req.RequestID,
Operation: req.Operation,
})
require.NoError(tb, err, "Marshal must not error")
return w.WriteMessage(gws.TextMessage, msg)
}
e = testexch.MockWsInstance[Exchange](t, testws.CurryWsMockUpgrader(t, mock))
e.Websocket.AuthConn = e.Websocket.Conn
err = e.Subscribe(subs)
require.NoError(t, err, "Subscribe must not error")
success = false
err = e.Subscribe(subs)
assert.ErrorContains(t, err, "Mock Resp Error", "Subscribe should error containing the returned RetMsg")
authsubs, err = e.generateAuthSubscriptions()
require.NoError(t, err, "generateAuthSubscriptions must not error")
require.NotEmpty(t, authsubs, "generateAuthSubscriptions must return subs")
require.NoError(t, e.authSubscribe(t.Context(), &FixtureConnection{}, authsubs))
require.NoError(t, e.authUnsubscribe(t.Context(), &FixtureConnection{}, authsubs))
}
func TestWebsocketAuthenticateConnection(t *testing.T) {
t.Parallel()
e := new(Exchange) //nolint:govet // Intentional shadow
require.NoError(t, testexch.Setup(e))
err := e.WebsocketAuthenticateConnection(t.Context(), &FixtureConnection{})
require.ErrorIs(t, err, exchange.ErrAuthenticationSupportNotEnabled)
e.API.AuthenticatedSupport = true
e.API.AuthenticatedWebsocketSupport = true
e.Websocket.SetCanUseAuthenticatedEndpoints(true)
ctx := account.DeployCredentialsToContext(t.Context(), &account.Credentials{Key: "dummy", Secret: "dummy"})
err = e.WebsocketAuthenticateConnection(ctx, &FixtureConnection{})
require.NoError(t, err)
err = e.WebsocketAuthenticateConnection(ctx, &FixtureConnection{sendMessageReturnResponseOverride: []byte(`{"success":false,"ret_msg":"failed auth","conn_id":"5758770c-8152-4545-a84f-dae089e56499","req_id":"1","op":"subscribe"}`)})
require.Error(t, err)
}
func TestTransformSymbol(t *testing.T) {
@@ -3730,3 +3888,55 @@ func TestTransformSymbol(t *testing.T) {
})
}
}
func TestMatchPairAssetFromResponse(t *testing.T) {
t.Parallel()
noDelim := currency.PairFormat{Uppercase: true}
for _, tc := range []struct {
pair string
category string
expectedAsset asset.Item
expectedPair currency.Pair
err error
}{
{pair: noDelim.Format(spotTradablePair), category: "spot", expectedAsset: asset.Spot, expectedPair: spotTradablePair},
{pair: noDelim.Format(usdtMarginedTradablePair), category: "linear", expectedAsset: asset.USDTMarginedFutures, expectedPair: usdtMarginedTradablePair},
{pair: noDelim.Format(usdcMarginedTradablePair), category: "linear", expectedAsset: asset.USDCMarginedFutures, expectedPair: usdcMarginedTradablePair},
{pair: noDelim.Format(inverseTradablePair), category: "inverse", expectedAsset: asset.CoinMarginedFutures, expectedPair: inverseTradablePair},
{pair: optionsTradablePair.String(), category: "option", expectedAsset: asset.Options, expectedPair: optionsTradablePair},
{pair: optionsTradablePair.String(), category: "silly", err: errUnsupportedCategory, expectedAsset: 0},
{pair: "bad pair", category: "spot", err: currency.ErrPairNotFound},
} {
t.Run(fmt.Sprintf("pair: %s, category: %s", tc.pair, tc.category), func(t *testing.T) {
t.Parallel()
p, a, err := e.matchPairAssetFromResponse(tc.category, tc.pair)
require.ErrorIs(t, err, tc.err)
assert.Equal(t, tc.expectedAsset, a)
assert.True(t, tc.expectedPair.Equal(p))
})
}
}
func TestHandleNoTopicWebsocketResponse(t *testing.T) {
t.Parallel()
for _, tc := range []struct {
operation string
requestID string
error error
}{
{operation: "subscribe"},
{operation: "unsubscribe"},
{operation: "auth"},
{operation: "auth", requestID: "noMatch", error: websocket.ErrSignatureNotMatched},
{operation: "ping"},
{operation: "pong"},
} {
t.Run(fmt.Sprintf("operation: %s, requestID: %s", tc.operation, tc.requestID), func(t *testing.T) {
t.Parallel()
err := e.handleNoTopicWebsocketResponse(&FixtureConnection{}, &WebsocketResponse{Operation: tc.operation, RequestID: tc.requestID}, nil)
assert.ErrorIs(t, err, tc.error, "handleNoTopicWebsocketResponse should return expected error")
})
}
}

View File

@@ -160,11 +160,23 @@ func constructOrderbook(o *orderbookResponse) (*Orderbook, error) {
// TickerData represents a list of ticker detailed information.
type TickerData struct {
Category string `json:"category"`
List []TickerItem `json:"list"`
List []TickerREST `json:"list"`
}
// TickerItem represents a ticker item detail
type TickerItem struct {
// TickerREST for REST API
type TickerREST struct {
TickerCommon
DeliveryTime types.Time `json:"deliveryTime"`
}
// TickerWebsocket for websocket API
type TickerWebsocket struct {
TickerCommon
DeliveryTime time.Time `json:"deliveryTime"` // "2025-03-28T08:00:00Z"
}
// TickerCommon common ticker fields
type TickerCommon struct {
Symbol string `json:"symbol"`
TickDirection string `json:"tickDirection"`
LastPrice types.Number `json:"lastPrice"`
@@ -1976,21 +1988,21 @@ type WebsocketWallet struct {
TotalInitialMargin types.Number `json:"totalInitialMargin"`
TotalMaintenanceMargin types.Number `json:"totalMaintenanceMargin"`
Coin []struct {
Coin string `json:"coin"`
Equity types.Number `json:"equity"`
UsdValue types.Number `json:"usdValue"`
WalletBalance types.Number `json:"walletBalance"`
AvailableToWithdraw types.Number `json:"availableToWithdraw"`
AvailableToBorrow types.Number `json:"availableToBorrow"`
BorrowAmount types.Number `json:"borrowAmount"`
AccruedInterest types.Number `json:"accruedInterest"`
TotalOrderIM types.Number `json:"totalOrderIM"`
TotalPositionIM types.Number `json:"totalPositionIM"`
TotalPositionMM types.Number `json:"totalPositionMM"`
UnrealisedPnl types.Number `json:"unrealisedPnl"`
CumRealisedPnl types.Number `json:"cumRealisedPnl"`
Bonus types.Number `json:"bonus"`
SpotHedgingQuantity types.Number `json:"spotHedgingQty"`
Coin currency.Code `json:"coin"`
Equity types.Number `json:"equity"`
UsdValue types.Number `json:"usdValue"`
WalletBalance types.Number `json:"walletBalance"`
AvailableToWithdraw types.Number `json:"availableToWithdraw"`
AvailableToBorrow types.Number `json:"availableToBorrow"`
BorrowAmount types.Number `json:"borrowAmount"`
AccruedInterest types.Number `json:"accruedInterest"`
TotalOrderIM types.Number `json:"totalOrderIM"`
TotalPositionIM types.Number `json:"totalPositionIM"`
TotalPositionMM types.Number `json:"totalPositionMM"`
UnrealisedPnl types.Number `json:"unrealisedPnl"`
CumRealisedPnl types.Number `json:"cumRealisedPnl"`
Bonus types.Number `json:"bonus"`
SpotHedgingQuantity types.Number `json:"spotHedgingQty"`
} `json:"coin"`
AccountType string `json:"accountType"`
AccountLTV string `json:"accountLTV"`
@@ -2003,11 +2015,11 @@ type GreeksResponse struct {
Topic string `json:"topic"`
CreationTime types.Time `json:"creationTime"`
Data []struct {
BaseCoin string `json:"baseCoin"`
TotalDelta types.Number `json:"totalDelta"`
TotalGamma types.Number `json:"totalGamma"`
TotalVega types.Number `json:"totalVega"`
TotalTheta types.Number `json:"totalTheta"`
BaseCoin currency.Code `json:"baseCoin"`
TotalDelta types.Number `json:"totalDelta"`
TotalGamma types.Number `json:"totalGamma"`
TotalVega types.Number `json:"totalVega"`
TotalTheta types.Number `json:"totalTheta"`
} `json:"data"`
}

View File

@@ -3,6 +3,7 @@ package bybit
import (
"context"
"encoding/hex"
"errors"
"fmt"
"net/http"
"strconv"
@@ -26,6 +27,7 @@ import (
"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"
)
const (
@@ -47,7 +49,7 @@ const (
chanOrder = "order"
chanWallet = "wallet"
chanGreeks = "greeks"
chanDCP = "dcp"
// TODO: Implement DCP (Disconnection Protect) subscription
spotPublic = "wss://stream.bybit.com/v5/public/spot"
linearPublic = "wss://stream.bybit.com/v5/public/linear" // USDT, USDC perpetual & USDC Futures
@@ -63,9 +65,8 @@ var defaultSubscriptions = subscription.List{
{Enabled: true, Asset: asset.Spot, Channel: subscription.OrderbookChannel, Levels: 50},
{Enabled: true, Asset: asset.Spot, Channel: subscription.AllTradesChannel},
{Enabled: true, Asset: asset.Spot, Channel: subscription.CandlesChannel, Interval: kline.OneHour},
{Enabled: true, Asset: asset.Spot, Authenticated: true, Channel: subscription.MyOrdersChannel},
{Enabled: true, Asset: asset.Spot, Authenticated: true, Channel: subscription.MyWalletChannel},
{Enabled: true, Asset: asset.Spot, Authenticated: true, Channel: subscription.MyTradesChannel},
// Authenticated channels are currently being managed by the `generateAuthSubscriptions` method for the private connection
// TODO: expand subscription template generation to handle authenticated subscriptions across all assets
}
var subscriptionNames = map[string]string{
@@ -73,84 +74,52 @@ var subscriptionNames = map[string]string{
subscription.OrderbookChannel: chanOrderbook,
subscription.AllTradesChannel: chanPublicTrade,
subscription.MyOrdersChannel: chanOrder,
subscription.MyTradesChannel: chanExecution,
subscription.MyWalletChannel: chanWallet,
subscription.MyTradesChannel: chanExecution,
subscription.CandlesChannel: chanKline,
}
var (
errUnhandledStreamData = errors.New("unhandled stream data")
errUnsupportedCategory = errors.New("unsupported category")
)
// WsConnect connects to a websocket feed
func (e *Exchange) WsConnect() error {
ctx := context.TODO()
if !e.Websocket.IsEnabled() || !e.IsEnabled() || !e.IsAssetWebsocketSupported(asset.Spot) {
return websocket.ErrWebsocketNotEnabled
}
var dialer gws.Dialer
err := e.Websocket.Conn.Dial(ctx, &dialer, http.Header{})
if err != nil {
func (e *Exchange) WsConnect(ctx context.Context, conn websocket.Connection) error {
if err := conn.Dial(ctx, &gws.Dialer{}, http.Header{}); err != nil {
return err
}
e.Websocket.Conn.SetupPingHandler(request.Unset, websocket.PingHandler{
conn.SetupPingHandler(request.Unset, websocket.PingHandler{
MessageType: gws.TextMessage,
Message: []byte(`{"op": "ping"}`),
Delay: bybitWebsocketTimer,
})
e.Websocket.Wg.Add(1)
go e.wsReadData(ctx, asset.Spot, e.Websocket.Conn)
if e.Websocket.CanUseAuthenticatedEndpoints() {
err = e.WsAuth(ctx)
if err != nil {
e.Websocket.DataHandler <- err
e.Websocket.SetCanUseAuthenticatedEndpoints(false)
}
}
return nil
}
// WsAuth sends an authentication message to receive auth data
func (e *Exchange) WsAuth(ctx context.Context) error {
// WebsocketAuthenticateConnection sends an authentication message to receive auth data
func (e *Exchange) WebsocketAuthenticateConnection(ctx context.Context, conn websocket.Connection) error {
creds, err := e.GetCredentials(ctx)
if err != nil {
return err
}
var dialer gws.Dialer
if err := e.Websocket.AuthConn.Dial(ctx, &dialer, http.Header{}); err != nil {
return err
}
e.Websocket.AuthConn.SetupPingHandler(request.Unset, websocket.PingHandler{
MessageType: gws.TextMessage,
Message: []byte(`{"op":"ping"}`),
Delay: bybitWebsocketTimer,
})
e.Websocket.Wg.Add(1)
go e.wsReadData(ctx, asset.Spot, e.Websocket.AuthConn)
intNonce := time.Now().Add(time.Hour * 6).UnixMilli()
strNonce := strconv.FormatInt(intNonce, 10)
hmac, err := crypto.GetHMAC(
crypto.HashSHA256,
[]byte("GET/realtime"+strNonce),
[]byte(creds.Secret),
)
hmac, err := crypto.GetHMAC(crypto.HashSHA256, []byte("GET/realtime"+strNonce), []byte(creds.Secret))
if err != nil {
return err
}
sign := hex.EncodeToString(hmac)
req := Authenticate{
RequestID: strconv.FormatInt(e.Websocket.AuthConn.GenerateMessageID(false), 10),
RequestID: strconv.FormatInt(conn.GenerateMessageID(false), 10),
Operation: "auth",
Args: []any{creds.Key, intNonce, sign},
Args: []any{creds.Key, intNonce, hex.EncodeToString(hmac)},
}
resp, err := e.Websocket.AuthConn.SendMessageReturnResponse(ctx, request.Unset, req.RequestID, req)
resp, err := conn.SendMessageReturnResponse(ctx, request.Unset, req.RequestID, req)
if err != nil {
return err
}
var response SubscriptionResponse
err = json.Unmarshal(resp, &response)
if err != nil {
if err := json.Unmarshal(resp, &response); err != nil {
return err
}
if !response.Success {
@@ -159,13 +128,7 @@ func (e *Exchange) WsAuth(ctx context.Context) error {
return nil
}
// Subscribe sends a websocket message to receive data from the channel
func (e *Exchange) Subscribe(channelsToSubscribe subscription.List) error {
ctx := context.TODO()
return e.handleSpotSubscription(ctx, "subscribe", channelsToSubscribe)
}
func (e *Exchange) handleSubscriptions(operation string, subs subscription.List) (args []SubscriptionArgument, err error) {
func (e *Exchange) handleSubscriptions(conn websocket.Connection, operation string, subs subscription.List) (args []SubscriptionArgument, err error) {
subs, err = subs.ExpandTemplates(e)
if err != nil {
return
@@ -176,68 +139,15 @@ func (e *Exchange) handleSubscriptions(operation string, subs subscription.List)
args = append(args, SubscriptionArgument{
auth: b[0].Authenticated,
Operation: operation,
RequestID: strconv.FormatInt(e.Websocket.Conn.GenerateMessageID(false), 10),
RequestID: strconv.FormatInt(conn.GenerateMessageID(false), 10),
Arguments: b.QualifiedChannels(),
associatedSubs: b,
})
}
}
return
}
// Unsubscribe sends a websocket message to stop receiving data from the channel
func (e *Exchange) Unsubscribe(channelsToUnsubscribe subscription.List) error {
ctx := context.TODO()
return e.handleSpotSubscription(ctx, "unsubscribe", channelsToUnsubscribe)
}
func (e *Exchange) handleSpotSubscription(ctx context.Context, operation string, channelsToSubscribe subscription.List) error {
payloads, err := e.handleSubscriptions(operation, channelsToSubscribe)
if err != nil {
return err
}
for a := range payloads {
var response []byte
if payloads[a].auth {
response, err = e.Websocket.AuthConn.SendMessageReturnResponse(ctx, request.Unset, payloads[a].RequestID, payloads[a])
if err != nil {
return err
}
} else {
response, err = e.Websocket.Conn.SendMessageReturnResponse(ctx, request.Unset, payloads[a].RequestID, payloads[a])
if err != nil {
return err
}
}
var resp SubscriptionResponse
err = json.Unmarshal(response, &resp)
if err != nil {
return err
}
if !resp.Success {
return fmt.Errorf("%s with request ID %s msg: %s", resp.Operation, resp.RequestID, resp.RetMsg)
}
var conn websocket.Connection
if payloads[a].auth {
conn = e.Websocket.AuthConn
} else {
conn = e.Websocket.Conn
}
if operation == "unsubscribe" {
err = e.Websocket.RemoveSubscriptions(conn, payloads[a].associatedSubs...)
} else {
err = e.Websocket.AddSubscriptions(conn, payloads[a].associatedSubs...)
}
if err != nil {
return err
}
}
return nil
}
// generateSubscriptions generates default subscription
func (e *Exchange) generateSubscriptions() (subscription.List, error) {
return e.Features.Subscriptions.ExpandTemplates(e)
@@ -246,61 +156,22 @@ func (e *Exchange) generateSubscriptions() (subscription.List, error) {
// GetSubscriptionTemplate returns a subscription channel template
func (e *Exchange) GetSubscriptionTemplate(_ *subscription.Subscription) (*template.Template, error) {
return template.New("master.tmpl").Funcs(template.FuncMap{
"channelName": channelName,
"isSymbolChannel": isSymbolChannel,
"intervalToString": intervalToString,
"getCategoryName": getCategoryName,
"isCategorisedChannel": isCategorisedChannel,
"channelName": channelName,
"isSymbolChannel": isSymbolChannel,
"intervalToString": intervalToString,
"getCategoryName": getCategoryName,
}).Parse(subTplText)
}
// wsReadData receives and passes on websocket messages for processing
func (e *Exchange) wsReadData(ctx context.Context, assetType asset.Item, ws websocket.Connection) {
defer e.Websocket.Wg.Done()
for {
select {
case <-e.Websocket.ShutdownC:
return
default:
resp := ws.ReadMessage()
if resp.Raw == nil {
return
}
err := e.wsHandleData(ctx, assetType, resp.Raw)
if err != nil {
e.Websocket.DataHandler <- err
}
}
}
}
func (e *Exchange) wsHandleData(ctx context.Context, assetType asset.Item, respRaw []byte) error {
func (e *Exchange) wsHandleData(conn websocket.Connection, assetType asset.Item, respRaw []byte) error {
var result WebsocketResponse
err := json.Unmarshal(respRaw, &result)
if err != nil {
if err := json.Unmarshal(respRaw, &result); err != nil {
return err
}
if result.Topic == "" {
switch result.Operation {
case "subscribe", "unsubscribe", "auth":
if result.RequestID != "" {
if !e.Websocket.Match.IncomingWithData(result.RequestID, respRaw) {
return fmt.Errorf("could not match subscription with id %s data %s", result.RequestID, respRaw)
}
}
case "ping", "pong":
default:
e.Websocket.DataHandler <- websocket.UnhandledMessageWarning{
Message: string(respRaw),
}
return nil
}
return nil
return e.handleNoTopicWebsocketResponse(conn, &result, respRaw)
}
topicSplit := strings.Split(result.Topic, ".")
if len(topicSplit) == 0 {
return errInvalidPushData
}
switch topicSplit[0] {
case chanOrderbook:
return e.wsProcessOrderbook(assetType, &result)
@@ -318,36 +189,59 @@ func (e *Exchange) wsHandleData(ctx context.Context, assetType asset.Item, respR
return e.wsProcessLeverageTokenTicker(assetType, &result)
case chanLeverageTokenNav:
return e.wsLeverageTokenNav(&result)
}
return fmt.Errorf("%w %s", errUnhandledStreamData, string(respRaw))
}
func (e *Exchange) wsHandleAuthenticatedData(ctx context.Context, conn websocket.Connection, respRaw []byte) error {
var result WebsocketResponse
if err := json.Unmarshal(respRaw, &result); err != nil {
return err
}
if result.Topic == "" {
return e.handleNoTopicWebsocketResponse(conn, &result, respRaw)
}
topicSplit := strings.Split(result.Topic, ".")
switch topicSplit[0] {
case chanPositions:
return e.wsProcessPosition(&result)
case chanExecution:
return e.wsProcessExecution(asset.Spot, &result)
return e.wsProcessExecution(&result)
case chanOrder:
return e.wsProcessOrder(asset.Spot, &result)
return e.wsProcessOrder(&result)
case chanWallet:
return e.wsProcessWalletPushData(ctx, asset.Spot, respRaw)
return e.wsProcessWalletPushData(ctx, respRaw)
case chanGreeks:
return e.wsProcessGreeks(respRaw)
case chanDCP:
return nil
}
return fmt.Errorf("unhandled stream data %s", string(respRaw))
return fmt.Errorf("%w %s", errUnhandledStreamData, string(respRaw))
}
func (e *Exchange) handleNoTopicWebsocketResponse(conn websocket.Connection, result *WebsocketResponse, respRaw []byte) error {
switch result.Operation {
case "subscribe", "unsubscribe", "auth":
if result.RequestID != "" {
return conn.RequireMatchWithData(result.RequestID, respRaw)
}
case "ping", "pong":
default:
e.Websocket.DataHandler <- websocket.UnhandledMessageWarning{Message: string(respRaw)}
}
return nil
}
func (e *Exchange) wsProcessGreeks(resp []byte) error {
var result GreeksResponse
err := json.Unmarshal(resp, &result)
if err != nil {
if err := json.Unmarshal(resp, &result); err != nil {
return err
}
e.Websocket.DataHandler <- &result
return nil
}
func (e *Exchange) wsProcessWalletPushData(ctx context.Context, assetType asset.Item, resp []byte) error {
func (e *Exchange) wsProcessWalletPushData(ctx context.Context, resp []byte) error {
var result WebsocketWallet
err := json.Unmarshal(resp, &result)
if err != nil {
if err := json.Unmarshal(resp, &result); err != nil {
return err
}
creds, err := e.GetCredentials(ctx)
@@ -358,9 +252,9 @@ func (e *Exchange) wsProcessWalletPushData(ctx context.Context, assetType asset.
for x := range result.Data {
for y := range result.Data[x].Coin {
changes = append(changes, account.Change{
AssetType: assetType,
AssetType: asset.Spot,
Balance: &account.Balance{
Currency: currency.NewCode(result.Data[x].Coin[y].Coin),
Currency: result.Data[x].Coin[y].Coin,
Total: result.Data[x].Coin[y].WalletBalance.Float64(),
Free: result.Data[x].Coin[y].WalletBalance.Float64(),
UpdatedAt: result.CreationTime.Time(),
@@ -373,15 +267,14 @@ func (e *Exchange) wsProcessWalletPushData(ctx context.Context, assetType asset.
}
// wsProcessOrder the order stream to see changes to your orders in real-time.
func (e *Exchange) wsProcessOrder(assetType asset.Item, resp *WebsocketResponse) error {
func (e *Exchange) wsProcessOrder(resp *WebsocketResponse) error {
var result WsOrders
err := json.Unmarshal(resp.Data, &result)
if err != nil {
if err := json.Unmarshal(resp.Data, &result); err != nil {
return err
}
execution := make([]order.Detail, len(result))
for x := range result {
cp, err := e.MatchSymbolWithAvailablePairs(result[x].Symbol, assetType, hasPotentialDelimiter(assetType))
cp, a, err := e.matchPairAssetFromResponse(result[x].Category, result[x].Symbol)
if err != nil {
return err
}
@@ -393,36 +286,42 @@ func (e *Exchange) wsProcessOrder(assetType asset.Item, resp *WebsocketResponse)
if err != nil {
return err
}
tif, err := order.StringToTimeInForce(result[x].TimeInForce)
if err != nil {
return err
}
execution[x] = order.Detail{
Amount: result[x].Qty.Float64(),
Exchange: e.Name,
OrderID: result[x].OrderID,
ClientOrderID: result[x].OrderLinkID,
Side: side,
Type: orderType,
Pair: cp,
Cost: result[x].CumExecQty.Float64() * result[x].AvgPrice.Float64(),
AssetType: assetType,
Status: StringToOrderStatus(result[x].OrderStatus),
Price: result[x].Price.Float64(),
ExecutedAmount: result[x].CumExecQty.Float64(),
Date: result[x].CreatedTime.Time(),
LastUpdated: result[x].UpdatedTime.Time(),
TimeInForce: tif,
Amount: result[x].Qty.Float64(),
Exchange: e.Name,
OrderID: result[x].OrderID,
ClientOrderID: result[x].OrderLinkID,
Side: side,
Type: orderType,
Pair: cp,
Cost: result[x].CumExecQty.Float64() * result[x].AvgPrice.Float64(),
Fee: result[x].CumExecFee.Float64(),
AssetType: a,
Status: StringToOrderStatus(result[x].OrderStatus),
Price: result[x].Price.Float64(),
ExecutedAmount: result[x].CumExecQty.Float64(),
AverageExecutedPrice: result[x].AvgPrice.Float64(),
Date: result[x].CreatedTime.Time(),
LastUpdated: result[x].UpdatedTime.Time(),
}
}
e.Websocket.DataHandler <- execution
return nil
}
func (e *Exchange) wsProcessExecution(assetType asset.Item, resp *WebsocketResponse) error {
func (e *Exchange) wsProcessExecution(resp *WebsocketResponse) error {
var result WsExecutions
err := json.Unmarshal(resp.Data, &result)
if err != nil {
if err := json.Unmarshal(resp.Data, &result); err != nil {
return err
}
executions := make([]fill.Data, len(result))
for x := range result {
cp, err := e.MatchSymbolWithAvailablePairs(result[x].Symbol, assetType, hasPotentialDelimiter(assetType))
cp, a, err := e.matchPairAssetFromResponse(result[x].Category, result[x].Symbol)
if err != nil {
return err
}
@@ -434,7 +333,7 @@ func (e *Exchange) wsProcessExecution(assetType asset.Item, resp *WebsocketRespo
ID: result[x].ExecID,
Timestamp: result[x].ExecTime.Time(),
Exchange: e.Name,
AssetType: assetType,
AssetType: a,
CurrencyPair: cp,
Side: side,
OrderID: result[x].OrderID,
@@ -449,8 +348,7 @@ func (e *Exchange) wsProcessExecution(assetType asset.Item, resp *WebsocketRespo
func (e *Exchange) wsProcessPosition(resp *WebsocketResponse) error {
var result WsPositions
err := json.Unmarshal(resp.Data, &result)
if err != nil {
if err := json.Unmarshal(resp.Data, &result); err != nil {
return err
}
e.Websocket.DataHandler <- result
@@ -459,8 +357,7 @@ func (e *Exchange) wsProcessPosition(resp *WebsocketResponse) error {
func (e *Exchange) wsLeverageTokenNav(resp *WebsocketResponse) error {
var result LTNav
err := json.Unmarshal(resp.Data, &result)
if err != nil {
if err := json.Unmarshal(resp.Data, &result); err != nil {
return err
}
e.Websocket.DataHandler <- result
@@ -468,9 +365,8 @@ func (e *Exchange) wsLeverageTokenNav(resp *WebsocketResponse) error {
}
func (e *Exchange) wsProcessLeverageTokenTicker(assetType asset.Item, resp *WebsocketResponse) error {
var result TickerItem
err := json.Unmarshal(resp.Data, &result)
if err != nil {
var result TickerWebsocket
if err := json.Unmarshal(resp.Data, &result); err != nil {
return err
}
cp, err := e.MatchSymbolWithAvailablePairs(result.Symbol, assetType, hasPotentialDelimiter(assetType))
@@ -491,8 +387,7 @@ func (e *Exchange) wsProcessLeverageTokenTicker(assetType asset.Item, resp *Webs
func (e *Exchange) wsProcessLeverageTokenKline(assetType asset.Item, resp *WebsocketResponse, topicSplit []string) error {
var result LTKlines
err := json.Unmarshal(resp.Data, &result)
if err != nil {
if err := json.Unmarshal(resp.Data, &result); err != nil {
return err
}
cp, err := e.MatchSymbolWithAvailablePairs(topicSplit[2], assetType, hasPotentialDelimiter(assetType))
@@ -525,8 +420,7 @@ func (e *Exchange) wsProcessLeverageTokenKline(assetType asset.Item, resp *Webso
func (e *Exchange) wsProcessLiquidation(resp *WebsocketResponse) error {
var result WebsocketLiquidation
err := json.Unmarshal(resp.Data, &result)
if err != nil {
if err := json.Unmarshal(resp.Data, &result); err != nil {
return err
}
e.Websocket.DataHandler <- result
@@ -535,8 +429,7 @@ func (e *Exchange) wsProcessLiquidation(resp *WebsocketResponse) error {
func (e *Exchange) wsProcessKline(assetType asset.Item, resp *WebsocketResponse, topicSplit []string) error {
var result WsKlines
err := json.Unmarshal(resp.Data, &result)
if err != nil {
if err := json.Unmarshal(resp.Data, &result); err != nil {
return err
}
cp, err := e.MatchSymbolWithAvailablePairs(topicSplit[2], assetType, hasPotentialDelimiter(assetType))
@@ -569,8 +462,8 @@ func (e *Exchange) wsProcessKline(assetType asset.Item, resp *WebsocketResponse,
}
func (e *Exchange) wsProcessPublicTicker(assetType asset.Item, resp *WebsocketResponse) error {
tickResp := new(TickerItem)
if err := json.Unmarshal(resp.Data, tickResp); err != nil {
var tickResp TickerWebsocket
if err := json.Unmarshal(resp.Data, &tickResp); err != nil {
return err
}
@@ -578,38 +471,25 @@ func (e *Exchange) wsProcessPublicTicker(assetType asset.Item, resp *WebsocketRe
if err != nil {
return err
}
pFmt, err := e.GetPairFormat(assetType, false)
if err != nil {
return err
}
p = p.Format(pFmt)
var tick *ticker.Price
if resp.Type == "snapshot" {
tick = &ticker.Price{
Pair: p,
ExchangeName: e.Name,
AssetType: assetType,
}
} else {
tick := &ticker.Price{Pair: p, ExchangeName: e.Name, AssetType: assetType}
if resp.Type != "snapshot" {
// ticker updates may be partial, so we need to update the current ticker
tick, err = ticker.GetTicker(e.Name, p, assetType)
tick, err = e.GetCachedTicker(p, assetType)
if err != nil {
return err
}
}
updateTicker(tick, tickResp)
updateTicker(tick, &tickResp)
tick.LastUpdated = resp.PushTimestamp.Time()
if err = ticker.ProcessTicker(tick); err == nil {
e.Websocket.DataHandler <- tick
if err := ticker.ProcessTicker(tick); err != nil {
return err
}
return err
e.Websocket.DataHandler <- tick
return nil
}
func updateTicker(tick *ticker.Price, resp *TickerItem) {
func updateTicker(tick *ticker.Price, resp *TickerWebsocket) {
if resp.LastPrice.Float64() != 0 {
tick.Last = resp.LastPrice.Float64()
}
@@ -669,8 +549,7 @@ func updateTicker(tick *ticker.Price, resp *TickerItem) {
func (e *Exchange) wsProcessPublicTrade(assetType asset.Item, resp *WebsocketResponse) error {
var result WebsocketPublicTrades
err := json.Unmarshal(resp.Data, &result)
if err != nil {
if err := json.Unmarshal(resp.Data, &result); err != nil {
return err
}
tradeDatas := make([]trade.Data, len(result))
@@ -755,20 +634,12 @@ func channelName(s *subscription.Subscription) string {
// isSymbolChannel returns whether the channel accepts a symbol parameter
func isSymbolChannel(name string) bool {
switch name {
case chanPositions, chanExecution, chanOrder, chanDCP, chanWallet:
case chanPositions, chanExecution, chanOrder, chanWallet:
return false
}
return true
}
func isCategorisedChannel(name string) bool {
switch name {
case chanPositions, chanExecution, chanOrder:
return true
}
return false
}
const subTplText = `
{{ with $name := channelName $.S }}
{{- range $asset, $pairs := $.AssetPairs }}
@@ -780,9 +651,6 @@ const subTplText = `
{{- $p }}
{{- $.PairSeparator }}
{{- end }}
{{- else }}
{{- $name }}
{{- if and (isCategorisedChannel $name) ($categoryName := getCategoryName $asset) -}} . {{- $categoryName -}} {{- end }}
{{- end }}
{{- end }}
{{- $.AssetSeparator }}
@@ -793,3 +661,172 @@ const subTplText = `
func hasPotentialDelimiter(a asset.Item) bool {
return a == asset.Options || a == asset.USDCMarginedFutures
}
// TODO: Remove this function when template expansion is across all assets
func (e *Exchange) submitDirectSubscription(ctx context.Context, conn websocket.Connection, a asset.Item, operation string, channelsToSubscribe subscription.List) error {
payloads, err := e.directSubscriptionPayload(conn, a, operation, channelsToSubscribe)
if err != nil {
return err
}
op := e.Websocket.AddSubscriptions
if operation == "unsubscribe" {
op = e.Websocket.RemoveSubscriptions
}
for _, payload := range payloads {
if a == asset.Options {
// The options connection does not send the subscription request id back with the subscription notification payload
// therefore the code doesn't wait for the response to check whether the subscription is successful or not.
if err := conn.SendJSONMessage(ctx, request.Unset, payload); err != nil {
return err
}
} else {
response, err := conn.SendMessageReturnResponse(ctx, request.Unset, payload.RequestID, payload)
if err != nil {
return err
}
var resp SubscriptionResponse
if err := json.Unmarshal(response, &resp); err != nil {
return err
}
if !resp.Success {
return fmt.Errorf("%s with request ID %s msg: %s", resp.Operation, resp.RequestID, resp.RetMsg)
}
}
if err := op(conn, payload.associatedSubs...); err != nil {
return err
}
}
return nil
}
// TODO: Remove this function when template expansion is across all assets
func (e *Exchange) directSubscriptionPayload(conn websocket.Connection, assetType asset.Item, operation string, channelsToSubscribe subscription.List) ([]SubscriptionArgument, error) {
var args []SubscriptionArgument
arg := SubscriptionArgument{
Operation: operation,
RequestID: strconv.FormatInt(conn.GenerateMessageID(false), 10),
Arguments: []string{},
}
authArg := SubscriptionArgument{
auth: true,
Operation: operation,
RequestID: strconv.FormatInt(conn.GenerateMessageID(false), 10),
Arguments: []string{},
}
chanMap := map[string]bool{}
pairFmt, err := e.GetPairFormat(assetType, true)
if err != nil {
return nil, err
}
for _, s := range channelsToSubscribe {
var pair currency.Pair
if len(s.Pairs) > 1 {
return nil, subscription.ErrNotSinglePair
}
if len(s.Pairs) == 1 {
pair = s.Pairs[0]
}
switch s.Channel {
case chanOrderbook:
arg.Arguments = append(arg.Arguments, fmt.Sprintf("%s.%d.%s", s.Channel, 50, pairFmt.Format(pair)))
arg.associatedSubs = append(arg.associatedSubs, s)
case chanPublicTrade, chanPublicTicker, chanLiquidation, chanLeverageTokenTicker, chanLeverageTokenNav:
arg.Arguments = append(arg.Arguments, s.Channel+"."+pairFmt.Format(pair))
arg.associatedSubs = append(arg.associatedSubs, s)
case chanKline, chanLeverageTokenKline:
interval, err := intervalToString(kline.FiveMin)
if err != nil {
return nil, err
}
arg.Arguments = append(arg.Arguments, s.Channel+"."+interval+"."+pairFmt.Format(pair))
arg.associatedSubs = append(arg.associatedSubs, s)
case chanPositions, chanExecution, chanOrder, chanWallet, chanGreeks:
if chanMap[s.Channel] {
continue
}
authArg.Arguments = append(authArg.Arguments, s.Channel)
// add channel name to map so we only subscribe to channel once
chanMap[s.Channel] = true
authArg.associatedSubs = append(authArg.associatedSubs, s)
}
if len(arg.Arguments) >= 10 {
args = append(args, arg)
arg = SubscriptionArgument{
Operation: operation,
RequestID: strconv.FormatInt(conn.GenerateMessageID(false), 10),
Arguments: []string{},
}
}
}
if len(arg.Arguments) != 0 {
args = append(args, arg)
}
if len(authArg.Arguments) != 0 {
args = append(args, authArg)
}
return args, nil
}
// generateAuthSubscriptions generates default subscription for the dedicated auth websocket connection. These are
// agnostic to the asset type and pair as all account level data will be routed through this connection.
// TODO: Remove this function when template expansion is across all assets
func (e *Exchange) generateAuthSubscriptions() (subscription.List, error) {
if !e.Websocket.CanUseAuthenticatedEndpoints() {
return nil, nil
}
for _, configSub := range e.Config.Features.Subscriptions.Enabled() {
if configSub.Authenticated {
log.Warnf(log.WebsocketMgr, "%s has an authenticated subscription %q in config which is not supported. Please remove.", e.Name, configSub.Channel)
configSub.Enabled = false
}
}
var subscriptions subscription.List
// TODO: Implement DCP (Disconnection Protect) subscription
for _, channel := range []string{chanPositions, chanExecution, chanOrder, chanWallet} {
subscriptions = append(subscriptions, &subscription.Subscription{Channel: channel, Asset: asset.All})
}
return subscriptions, nil
}
func (e *Exchange) authSubscribe(ctx context.Context, conn websocket.Connection, channelSubscriptions subscription.List) error {
return e.submitDirectSubscription(ctx, conn, asset.Spot, "subscribe", channelSubscriptions)
}
func (e *Exchange) authUnsubscribe(ctx context.Context, conn websocket.Connection, channelSubscriptions subscription.List) error {
return e.submitDirectSubscription(ctx, conn, asset.Spot, "unsubscribe", channelSubscriptions)
}
// matchPairAssetFromResponse returns the currency pair and asset type based on the category and symbol. Used with a dedicated
// auth connection where multiple asset type changes are piped through a single connection.
func (e *Exchange) matchPairAssetFromResponse(category, symbol string) (currency.Pair, asset.Item, error) {
assets := make([]asset.Item, 0, 2)
switch category {
case "spot":
assets = append(assets, asset.Spot)
case "inverse":
assets = append(assets, asset.CoinMarginedFutures)
case "linear":
assets = append(assets, asset.USDTMarginedFutures, asset.USDCMarginedFutures)
case "option":
assets = append(assets, asset.Options)
default:
return currency.EMPTYPAIR, 0, fmt.Errorf("incoming symbol %q %w: %q", symbol, errUnsupportedCategory, category)
}
for _, a := range assets {
cp, err := e.MatchSymbolWithAvailablePairs(symbol, a, hasPotentialDelimiter(a))
if err != nil {
if !errors.Is(err, currency.ErrPairNotFound) {
return currency.EMPTYPAIR, 0, fmt.Errorf("%w for symbol %q: %q", err, category, symbol)
}
continue
}
return cp, a, nil
}
return currency.EMPTYPAIR, 0, currency.ErrPairNotFound
}

View File

@@ -27,6 +27,7 @@ import (
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
"github.com/thrasher-corp/gocryptotrader/exchanges/protocol"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"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"
@@ -67,12 +68,6 @@ func (e *Exchange) SetDefaults() {
}
}
for _, a := range []asset.Item{asset.CoinMarginedFutures, asset.USDTMarginedFutures, asset.USDCMarginedFutures, asset.Options} {
if err := e.DisableAssetWebsocketSupport(a); err != nil {
log.Errorf(log.ExchangeSys, "%s error disabling %q asset type websocket support: %s", e.Name, a, err)
}
}
e.Features = exchange.Features{
CurrencyTranslations: currency.NewTranslations(
map[currency.Code]currency.Code{
@@ -188,12 +183,17 @@ func (e *Exchange) SetDefaults() {
e.API.Endpoints = e.NewEndpoints()
err := e.API.Endpoints.SetDefaultEndpoints(map[exchange.URL]string{
exchange.RestSpot: bybitAPIURL,
exchange.RestCoinMargined: bybitAPIURL,
exchange.RestUSDTMargined: bybitAPIURL,
exchange.RestFutures: bybitAPIURL,
exchange.RestUSDCMargined: bybitAPIURL,
exchange.WebsocketSpot: spotPublic,
exchange.RestSpot: bybitAPIURL,
exchange.RestCoinMargined: bybitAPIURL,
exchange.RestUSDTMargined: bybitAPIURL,
exchange.RestFutures: bybitAPIURL,
exchange.RestUSDCMargined: bybitAPIURL,
exchange.WebsocketSpot: spotPublic,
exchange.WebsocketCoinMargined: inversePublic,
exchange.WebsocketUSDTMargined: linearPublic,
exchange.WebsocketUSDCMargined: linearPublic,
exchange.WebsocketOptions: optionPublic,
exchange.WebsocketPrivate: websocketPrivate,
})
if err != nil {
log.Errorln(log.ExchangeSys, err)
@@ -214,65 +214,176 @@ func (e *Exchange) SetDefaults() {
// Setup takes in the supplied exchange configuration details and sets params
func (e *Exchange) Setup(exch *config.Exchange) error {
err := exch.Validate()
if err != nil {
if err := exch.Validate(); err != nil {
return err
}
if !exch.Enabled {
e.SetEnabled(false)
return nil
}
if err := e.SetupDefaults(exch); err != nil {
return err
}
err = e.SetupDefaults(exch)
if err := e.Websocket.Setup(&websocket.ManagerSetup{
ExchangeConfig: exch,
Features: &e.Features.Supports.WebsocketCapabilities,
OrderbookBufferConfig: buffer.Config{SortBuffer: true, SortBufferByUpdateIDs: true},
TradeFeed: e.Features.Enabled.TradeFeed,
UseMultiConnectionManagement: true,
}); err != nil {
return err
}
wsSpotURL, err := e.API.Endpoints.GetURL(exchange.WebsocketSpot)
if err != nil {
return err
}
wsRunningEndpoint, err := e.API.Endpoints.GetURL(exchange.WebsocketSpot)
// Spot
if err := e.Websocket.SetupNewConnection(&websocket.ConnectionSetup{
URL: wsSpotURL,
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
ResponseMaxLimit: exch.WebsocketResponseMaxLimit,
RateLimit: request.NewWeightedRateLimitByDuration(time.Microsecond),
Connector: e.WsConnect,
GenerateSubscriptions: e.generateSubscriptions,
Subscriber: e.SpotSubscribe,
Unsubscriber: e.SpotUnsubscribe,
Handler: func(_ context.Context, conn websocket.Connection, resp []byte) error {
return e.wsHandleData(conn, asset.Spot, resp)
},
RequestIDGenerator: e.messageIDSeq.IncrementAndGet,
}); err != nil {
return err
}
wsOptionsURL, err := e.API.Endpoints.GetURL(exchange.WebsocketOptions)
if err != nil {
return err
}
err = e.Websocket.Setup(
&websocket.ManagerSetup{
ExchangeConfig: exch,
DefaultURL: spotPublic,
RunningURL: wsRunningEndpoint,
RunningURLAuth: websocketPrivate,
Connector: e.WsConnect,
Subscriber: e.Subscribe,
Unsubscriber: e.Unsubscribe,
GenerateSubscriptions: e.generateSubscriptions,
Features: &e.Features.Supports.WebsocketCapabilities,
OrderbookBufferConfig: buffer.Config{
SortBuffer: true,
SortBufferByUpdateIDs: true,
},
TradeFeed: e.Features.Enabled.TradeFeed,
})
if err != nil {
// Options
if err := e.Websocket.SetupNewConnection(&websocket.ConnectionSetup{
URL: wsOptionsURL,
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
ResponseMaxLimit: exch.WebsocketResponseMaxLimit,
RateLimit: request.NewWeightedRateLimitByDuration(time.Microsecond),
Connector: e.WsConnect,
GenerateSubscriptions: e.GenerateOptionsDefaultSubscriptions,
Subscriber: e.OptionsSubscribe,
Unsubscriber: e.OptionsUnsubscribe,
Handler: func(_ context.Context, conn websocket.Connection, resp []byte) error {
return e.wsHandleData(conn, asset.Options, resp)
},
RequestIDGenerator: e.messageIDSeq.IncrementAndGet,
}); err != nil {
return err
}
err = e.Websocket.SetupNewConnection(&websocket.ConnectionSetup{
URL: e.Websocket.GetWebsocketURL(),
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
ResponseMaxLimit: bybitWebsocketTimer,
})
wsUSDTLinearURL, err := e.API.Endpoints.GetURL(exchange.WebsocketUSDTMargined)
if err != nil {
return err
}
return e.Websocket.SetupNewConnection(&websocket.ConnectionSetup{
URL: websocketPrivate,
// Linear - USDT margined futures.
if err := e.Websocket.SetupNewConnection(&websocket.ConnectionSetup{
URL: wsUSDTLinearURL,
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
ResponseMaxLimit: exch.WebsocketResponseMaxLimit,
Authenticated: true,
})
}
RateLimit: request.NewWeightedRateLimitByDuration(time.Microsecond),
Connector: e.WsConnect,
GenerateSubscriptions: func() (subscription.List, error) {
return e.GenerateLinearDefaultSubscriptions(asset.USDTMarginedFutures)
},
Subscriber: func(ctx context.Context, conn websocket.Connection, sub subscription.List) error {
return e.LinearSubscribe(ctx, conn, asset.USDTMarginedFutures, sub)
},
Unsubscriber: func(ctx context.Context, conn websocket.Connection, unsub subscription.List) error {
return e.LinearUnsubscribe(ctx, conn, asset.USDTMarginedFutures, unsub)
},
Handler: func(_ context.Context, conn websocket.Connection, resp []byte) error {
return e.wsHandleData(conn, asset.USDTMarginedFutures, resp)
},
RequestIDGenerator: e.messageIDSeq.IncrementAndGet,
MessageFilter: asset.USDTMarginedFutures, // Unused but it allows us to differentiate between the two linear futures types.
}); err != nil {
return err
}
// AuthenticateWebsocket sends an authentication message to the websocket
func (e *Exchange) AuthenticateWebsocket(ctx context.Context) error {
return e.WsAuth(ctx)
wsUSDCLinearURL, err := e.API.Endpoints.GetURL(exchange.WebsocketUSDCMargined)
if err != nil {
return err
}
// Linear - USDC margined futures.
if err := e.Websocket.SetupNewConnection(&websocket.ConnectionSetup{
URL: wsUSDCLinearURL,
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
ResponseMaxLimit: exch.WebsocketResponseMaxLimit,
RateLimit: request.NewWeightedRateLimitByDuration(time.Microsecond),
Connector: e.WsConnect,
GenerateSubscriptions: func() (subscription.List, error) {
return e.GenerateLinearDefaultSubscriptions(asset.USDCMarginedFutures)
},
Subscriber: func(ctx context.Context, conn websocket.Connection, sub subscription.List) error {
return e.LinearSubscribe(ctx, conn, asset.USDCMarginedFutures, sub)
},
Unsubscriber: func(ctx context.Context, conn websocket.Connection, unsub subscription.List) error {
return e.LinearUnsubscribe(ctx, conn, asset.USDCMarginedFutures, unsub)
},
Handler: func(_ context.Context, conn websocket.Connection, resp []byte) error {
return e.wsHandleData(conn, asset.USDCMarginedFutures, resp)
},
RequestIDGenerator: e.messageIDSeq.IncrementAndGet,
MessageFilter: asset.USDCMarginedFutures, // Unused but it allows us to differentiate between the two linear futures types.
}); err != nil {
return err
}
wsInverseURL, err := e.API.Endpoints.GetURL(exchange.WebsocketCoinMargined)
if err != nil {
return err
}
// Inverse - Coin margined futures.
if err := e.Websocket.SetupNewConnection(&websocket.ConnectionSetup{
URL: wsInverseURL,
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
ResponseMaxLimit: exch.WebsocketResponseMaxLimit,
RateLimit: request.NewWeightedRateLimitByDuration(time.Microsecond),
Connector: e.WsConnect,
GenerateSubscriptions: e.GenerateInverseDefaultSubscriptions,
Subscriber: e.InverseSubscribe,
Unsubscriber: e.InverseUnsubscribe,
Handler: func(_ context.Context, conn websocket.Connection, resp []byte) error {
return e.wsHandleData(conn, asset.CoinMarginedFutures, resp)
},
RequestIDGenerator: e.messageIDSeq.IncrementAndGet,
}); err != nil {
return err
}
wsPrivateURL, err := e.API.Endpoints.GetURL(exchange.WebsocketPrivate)
if err != nil {
return err
}
// Private
return e.Websocket.SetupNewConnection(&websocket.ConnectionSetup{
URL: wsPrivateURL,
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
ResponseMaxLimit: exch.WebsocketResponseMaxLimit,
RateLimit: request.NewWeightedRateLimitByDuration(time.Microsecond),
Authenticated: true,
Connector: e.WsConnect,
GenerateSubscriptions: e.generateAuthSubscriptions,
Subscriber: e.authSubscribe,
Unsubscriber: e.authUnsubscribe,
Handler: e.wsHandleAuthenticatedData,
RequestIDGenerator: e.messageIDSeq.IncrementAndGet,
Authenticate: e.WebsocketAuthenticateConnection,
})
}
// FetchTradablePairs returns a list of the exchanges tradable pairs

View File

@@ -0,0 +1,44 @@
package bybit
import (
"context"
"errors"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchange/websocket"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/subscription"
)
// GenerateInverseDefaultSubscriptions generates default subscription
func (e *Exchange) GenerateInverseDefaultSubscriptions() (subscription.List, error) {
pairs, err := e.GetEnabledPairs(asset.CoinMarginedFutures)
if err != nil {
if errors.Is(err, asset.ErrNotEnabled) {
return nil, nil
}
return nil, err
}
var subscriptions subscription.List
for z := range pairs {
for _, channel := range []string{chanOrderbook, chanPublicTrade, chanPublicTicker} {
subscriptions = append(subscriptions, &subscription.Subscription{
Channel: channel,
Pairs: currency.Pairs{pairs[z]},
Asset: asset.CoinMarginedFutures,
})
}
}
return subscriptions, nil
}
// InverseSubscribe sends a websocket message to receive data from the channel
func (e *Exchange) InverseSubscribe(ctx context.Context, conn websocket.Connection, channelSubscriptions subscription.List) error {
return e.submitDirectSubscription(ctx, conn, asset.CoinMarginedFutures, "subscribe", channelSubscriptions)
}
// InverseUnsubscribe sends a websocket message to stop receiving data from the channel
func (e *Exchange) InverseUnsubscribe(ctx context.Context, conn websocket.Connection, channelSubscriptions subscription.List) error {
return e.submitDirectSubscription(ctx, conn, asset.CoinMarginedFutures, "unsubscribe", channelSubscriptions)
}

View File

@@ -0,0 +1,58 @@
package bybit
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
testexch "github.com/thrasher-corp/gocryptotrader/internal/testing/exchange"
)
func TestGenerateInverseDefaultSubscriptions(t *testing.T) {
t.Parallel()
e := new(Exchange) //nolint:govet // Intentional shadow
require.NoError(t, testexch.Setup(e), "Test instance Setup must not error")
subs, err := e.GenerateInverseDefaultSubscriptions()
require.NoError(t, err, "GenerateInverseDefaultSubscriptions must not error")
assert.NotEmpty(t, subs, "Subscriptions should not be empty")
for i := range subs {
assert.Equal(t, asset.CoinMarginedFutures, subs[i].Asset, "Asset type should be CoinMarginedFutures")
}
err = e.CurrencyPairs.SetAssetEnabled(asset.CoinMarginedFutures, false)
require.NoError(t, err, "SetAssetEnabled must not error")
subs, err = e.GenerateInverseDefaultSubscriptions()
require.NoError(t, err, "GenerateInverseDefaultSubscriptions must not error")
assert.Empty(t, subs, "Subscriptions should be empty when asset is disabled")
}
func TestInverseSubscribe(t *testing.T) {
t.Parallel()
e := new(Exchange) //nolint:govet // Intentional shadow
require.NoError(t, testexch.Setup(e), "Test instance Setup must not error")
subs, err := e.GenerateInverseDefaultSubscriptions()
require.NoError(t, err, "GenerateInverseDefaultSubscriptions must not error")
err = e.InverseSubscribe(t.Context(), &FixtureConnection{}, subs)
require.NoError(t, err, "InverseSubscribe must not error")
}
func TestInverseUnsubscribe(t *testing.T) {
t.Parallel()
e := new(Exchange) //nolint:govet // Intentional shadow
require.NoError(t, testexch.Setup(e), "Test instance Setup must not error")
subs, err := e.GenerateInverseDefaultSubscriptions()
require.NoError(t, err, "GenerateInverseDefaultSubscriptions must not error")
err = e.InverseSubscribe(t.Context(), &FixtureConnection{}, subs)
require.NoError(t, err, "InverseSubscribe must not error")
err = e.InverseUnsubscribe(t.Context(), &FixtureConnection{}, subs)
require.NoError(t, err, "InverseUnsubscribe must not error")
}

View File

@@ -0,0 +1,61 @@
package bybit
import (
"context"
"errors"
"fmt"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchange/websocket"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/subscription"
)
// GenerateLinearDefaultSubscriptions generates default subscription
func (e *Exchange) GenerateLinearDefaultSubscriptions(a asset.Item) (subscription.List, error) {
if err := checkLinearAsset(a); err != nil {
return nil, err
}
pairs, err := e.GetEnabledPairs(a)
if err != nil {
if errors.Is(err, asset.ErrNotEnabled) {
return nil, nil
}
return nil, err
}
var subscriptions subscription.List
for _, pair := range pairs {
for _, channel := range []string{chanOrderbook, chanPublicTrade, chanPublicTicker} {
subscriptions = append(subscriptions, &subscription.Subscription{
Channel: channel,
Pairs: currency.Pairs{pair},
Asset: a,
})
}
}
return subscriptions, nil
}
// LinearSubscribe sends a websocket message to receive data from the channel
func (e *Exchange) LinearSubscribe(ctx context.Context, conn websocket.Connection, a asset.Item, channelSubscriptions subscription.List) error {
if err := checkLinearAsset(a); err != nil {
return err
}
return e.submitDirectSubscription(ctx, conn, a, "subscribe", channelSubscriptions)
}
// LinearUnsubscribe sends a websocket message to stop receiving data from the channel
func (e *Exchange) LinearUnsubscribe(ctx context.Context, conn websocket.Connection, a asset.Item, channelSubscriptions subscription.List) error {
if err := checkLinearAsset(a); err != nil {
return err
}
return e.submitDirectSubscription(ctx, conn, a, "unsubscribe", channelSubscriptions)
}
func checkLinearAsset(a asset.Item) error {
if a != asset.USDTMarginedFutures && a != asset.USDCMarginedFutures {
return fmt.Errorf("%q %w for linear subscriptions", a, asset.ErrInvalidAsset)
}
return nil
}

View File

@@ -0,0 +1,78 @@
package bybit
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
testexch "github.com/thrasher-corp/gocryptotrader/internal/testing/exchange"
)
func TestGenerateLinearDefaultSubscriptions(t *testing.T) {
t.Parallel()
_, err := e.GenerateLinearDefaultSubscriptions(asset.OptionCombo)
assert.ErrorIs(t, err, asset.ErrInvalidAsset)
e := new(Exchange)
require.NoError(t, testexch.Setup(e), "Test instance Setup must not error")
subs, err := e.GenerateLinearDefaultSubscriptions(asset.USDTMarginedFutures)
require.NoError(t, err, "GenerateLinearDefaultSubscriptions must not error")
assert.NotEmpty(t, subs, "Subscriptions should not be empty")
for i := range subs {
assert.Equal(t, asset.USDTMarginedFutures, subs[i].Asset, "Asset type should be USDTMarginedFutures")
}
err = e.CurrencyPairs.SetAssetEnabled(asset.USDTMarginedFutures, false)
require.NoError(t, err, "SetAssetEnabled must not error")
subs, err = e.GenerateLinearDefaultSubscriptions(asset.USDTMarginedFutures)
require.NoError(t, err, "GenerateLinearDefaultSubscriptions must not error")
assert.Empty(t, subs, "Subscriptions should be empty when asset is disabled")
subs, err = e.GenerateLinearDefaultSubscriptions(asset.USDCMarginedFutures)
require.NoError(t, err, "GenerateLinearDefaultSubscriptions must not error")
assert.NotEmpty(t, subs, "Subscriptions should not be empty")
for i := range subs {
assert.Equal(t, asset.USDCMarginedFutures, subs[i].Asset, "Asset type should be USDCMarginedFutures")
}
}
func TestLinearSubscribe(t *testing.T) {
t.Parallel()
e := new(Exchange)
require.NoError(t, testexch.Setup(e), "Test instance Setup must not error")
subs, err := e.GenerateLinearDefaultSubscriptions(asset.USDTMarginedFutures)
require.NoError(t, err, "GenerateLinearDefaultSubscriptions must not error")
assert.NotEmpty(t, subs, "Subscriptions should not be empty")
err = e.LinearSubscribe(t.Context(), &FixtureConnection{}, asset.OptionCombo, subs)
require.ErrorIs(t, err, asset.ErrInvalidAsset)
err = e.LinearSubscribe(t.Context(), &FixtureConnection{}, asset.USDTMarginedFutures, subs)
require.NoError(t, err, "LinearSubscribe must not error")
}
func TestLinearUnsubscribe(t *testing.T) {
t.Parallel()
e := new(Exchange)
require.NoError(t, testexch.Setup(e), "Test instance Setup must not error")
subs, err := e.GenerateLinearDefaultSubscriptions(asset.USDTMarginedFutures)
require.NoError(t, err, "GenerateLinearDefaultSubscriptions must not error")
assert.NotEmpty(t, subs, "Subscriptions should not be empty")
err = e.LinearSubscribe(t.Context(), &FixtureConnection{}, asset.USDTMarginedFutures, subs)
require.NoError(t, err, "LinearSubscribe must not error")
err = e.LinearUnsubscribe(t.Context(), &FixtureConnection{}, asset.OptionCombo, subs)
require.ErrorIs(t, err, asset.ErrInvalidAsset)
err = e.LinearUnsubscribe(t.Context(), &FixtureConnection{}, asset.USDTMarginedFutures, subs)
require.NoError(t, err, "LinearUnsubscribe must not error")
}

View File

@@ -0,0 +1,44 @@
package bybit
import (
"context"
"errors"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchange/websocket"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/subscription"
)
// GenerateOptionsDefaultSubscriptions generates default subscription
func (e *Exchange) GenerateOptionsDefaultSubscriptions() (subscription.List, error) {
pairs, err := e.GetEnabledPairs(asset.Options)
if err != nil {
if errors.Is(err, asset.ErrNotEnabled) {
return nil, nil
}
return nil, err
}
var subscriptions subscription.List
for z := range pairs {
for _, channel := range []string{chanOrderbook, chanPublicTrade, chanPublicTicker} {
subscriptions = append(subscriptions, &subscription.Subscription{
Channel: channel,
Pairs: currency.Pairs{pairs[z]},
Asset: asset.Options,
})
}
}
return subscriptions, nil
}
// OptionsSubscribe sends a websocket message to receive data from the channel
func (e *Exchange) OptionsSubscribe(ctx context.Context, conn websocket.Connection, channelSubscriptions subscription.List) error {
return e.submitDirectSubscription(ctx, conn, asset.Options, "subscribe", channelSubscriptions)
}
// OptionsUnsubscribe sends a websocket message to stop receiving data from the channel
func (e *Exchange) OptionsUnsubscribe(ctx context.Context, conn websocket.Connection, channelSubscriptions subscription.List) error {
return e.submitDirectSubscription(ctx, conn, asset.Options, "unsubscribe", channelSubscriptions)
}

View File

@@ -0,0 +1,58 @@
package bybit
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
testexch "github.com/thrasher-corp/gocryptotrader/internal/testing/exchange"
)
func TestGenerateOptionsDefaultSubscriptions(t *testing.T) {
t.Parallel()
e := new(Exchange)
require.NoError(t, testexch.Setup(e), "Test instance Setup must not error")
subs, err := e.GenerateOptionsDefaultSubscriptions()
require.NoError(t, err, "GenerateOptionsDefaultSubscriptions must not error")
assert.NotEmpty(t, subs, "Subscriptions should not be empty")
for i := range subs {
assert.Equal(t, asset.Options, subs[i].Asset, "Asset type should be Options")
}
err = e.CurrencyPairs.SetAssetEnabled(asset.Options, false)
require.NoError(t, err, "SetAssetEnabled must not error")
subs, err = e.GenerateOptionsDefaultSubscriptions()
require.NoError(t, err, "GenerateOptionsDefaultSubscriptions must not error")
assert.Empty(t, subs, "Subscriptions should be empty when asset is disabled")
}
func TestOptionSubscribe(t *testing.T) {
t.Parallel()
e := new(Exchange)
require.NoError(t, testexch.Setup(e), "Test instance Setup must not error")
subs, err := e.GenerateOptionsDefaultSubscriptions()
require.NoError(t, err, "GenerateOptionsDefaultSubscriptions must not error")
err = e.OptionsSubscribe(t.Context(), &FixtureConnection{}, subs)
require.NoError(t, err, "OptionsSubscribe must not error")
}
func TestOptionsUnsubscribe(t *testing.T) {
t.Parallel()
e := new(Exchange)
require.NoError(t, testexch.Setup(e), "Test instance Setup must not error")
subs, err := e.GenerateOptionsDefaultSubscriptions()
require.NoError(t, err, "GenerateOptionsDefaultSubscriptions must not error")
err = e.OptionsSubscribe(t.Context(), &FixtureConnection{}, subs)
require.NoError(t, err, "OptionsSubscribe must not error")
err = e.OptionsUnsubscribe(t.Context(), &FixtureConnection{}, subs)
require.NoError(t, err, "OptionsUnsubscribe must not error")
}

View File

@@ -0,0 +1,50 @@
package bybit
import (
"context"
"fmt"
"github.com/thrasher-corp/gocryptotrader/encoding/json"
"github.com/thrasher-corp/gocryptotrader/exchange/websocket"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"github.com/thrasher-corp/gocryptotrader/exchanges/subscription"
)
func (e *Exchange) handleSpotSubscription(ctx context.Context, conn websocket.Connection, operation string, channelsToSubscribe subscription.List) error {
payloads, err := e.handleSubscriptions(conn, operation, channelsToSubscribe)
if err != nil {
return err
}
for _, payload := range payloads {
response, err := conn.SendMessageReturnResponse(ctx, request.Unset, payload.RequestID, payload)
if err != nil {
return err
}
var resp SubscriptionResponse
if err := json.Unmarshal(response, &resp); err != nil {
return err
}
if !resp.Success {
return fmt.Errorf("%s with request ID %s msg: %s", resp.Operation, resp.RequestID, resp.RetMsg)
}
if operation == "unsubscribe" {
err = e.Websocket.RemoveSubscriptions(conn, payload.associatedSubs...)
} else {
err = e.Websocket.AddSubscriptions(conn, payload.associatedSubs...)
}
if err != nil {
return err
}
}
return nil
}
// SpotSubscribe sends a websocket message to receive data from the channel
func (e *Exchange) SpotSubscribe(ctx context.Context, conn websocket.Connection, channelsToSubscribe subscription.List) error {
return e.handleSpotSubscription(ctx, conn, "subscribe", channelsToSubscribe)
}
// SpotUnsubscribe sends a websocket message to stop receiving data from the channel
func (e *Exchange) SpotUnsubscribe(ctx context.Context, conn websocket.Connection, channelsToUnsubscribe subscription.List) error {
return e.handleSpotSubscription(ctx, conn, "unsubscribe", channelsToUnsubscribe)
}

View File

@@ -0,0 +1,30 @@
package bybit
import (
"testing"
"github.com/stretchr/testify/require"
testexch "github.com/thrasher-corp/gocryptotrader/internal/testing/exchange"
)
func TestSpotSubscribe(t *testing.T) {
t.Parallel()
e := new(Exchange)
require.NoError(t, testexch.Setup(e), "Test instance Setup must not error")
subs, err := e.Features.Subscriptions.ExpandTemplates(e)
require.NoError(t, err, "ExpandTemplates must not error")
err = e.SpotSubscribe(t.Context(), &FixtureConnection{}, subs)
require.NoError(t, err, "Subscribe must not error")
}
func TestSpotUnsubscribe(t *testing.T) {
t.Parallel()
e := new(Exchange)
require.NoError(t, testexch.Setup(e), "Test instance Setup must not error")
subs, err := e.Features.Subscriptions.ExpandTemplates(e)
require.NoError(t, err, "ExpandTemplates must not error")
err = e.SpotSubscribe(t.Context(), &FixtureConnection{}, subs)
require.NoError(t, err, "Subscribe must not error")
err = e.SpotUnsubscribe(t.Context(), &FixtureConnection{}, subs)
require.NoError(t, err, "Unsubscribe must not error")
}

6
exchanges/bybit/testdata/wsAuth.json vendored Normal file
View File

@@ -0,0 +1,6 @@
{"id": "59232430b58efe-5fc5-4470-9337-4ce293b68edd", "topic": "position", "creationTime": 1672364174455, "data": [ { "positionIdx": 0, "tradeMode": 0, "riskId": 41, "riskLimitValue": "200000", "symbol": "XRPUSDT", "side": "Buy", "size": "75", "entryPrice": "0.3615", "leverage": "10", "positionValue": "27.1125", "positionBalance": "0", "markPrice": "0.3374", "positionIM": "2.72589075", "positionMM": "0.28576575", "takeProfit": "0", "stopLoss": "0", "trailingStop": "0", "unrealisedPnl": "-1.8075", "cumRealisedPnl": "0.64782276", "createdTime": "1672121182216", "updatedTime": "1672364174449", "tpslMode": "Full", "liqPrice": "", "bustPrice": "", "category": "linear","positionStatus":"Normal","adlRankIndicator":2}]}
{ "id": "5923240c6880ab-c59f-420b-9adb-3639adc9dd90", "topic": "order", "creationTime": 1672364262474, "data": [ { "symbol": "%s", "orderId": "5cf98598-39a7-459e-97bf-76ca765ee020", "side": "Sell", "orderType": "Market", "cancelType": "UNKNOWN", "price": "72.5", "qty": "1", "orderIv": "", "timeInForce": "IOC", "orderStatus": "Filled", "orderLinkId": "", "lastPriceOnCreated": "", "reduceOnly": false, "leavesQty": "", "leavesValue": "", "cumExecQty": "1", "cumExecValue": "75", "avgPrice": "75", "blockTradeId": "", "positionIdx": 0, "cumExecFee": "0.358635", "createdTime": "1672364262444", "updatedTime": "1672364262457", "rejectReason": "EC_NoError", "stopOrderType": "", "tpslMode": "", "triggerPrice": "", "takeProfit": "", "stopLoss": "", "tpTriggerBy": "", "slTriggerBy": "", "tpLimitPrice": "", "slLimitPrice": "", "triggerDirection": 0, "triggerBy": "", "closeOnTrigger": false, "category": "option", "placeType": "price", "smpType": "None", "smpGroup": 0, "smpOrderId": "" } ] }
{ "id": "5923242c464be9-25ca-483d-a743-c60101fc656f", "topic": "wallet", "creationTime": 1672364262482, "data": [ { "accountIMRate": "0.016", "accountMMRate": "0.003", "totalEquity": "12837.78330098", "totalWalletBalance": "12840.4045924", "totalMarginBalance": "12837.78330188", "totalAvailableBalance": "12632.05767702", "totalPerpUPL": "-2.62129051", "totalInitialMargin": "205.72562486", "totalMaintenanceMargin": "39.42876721", "coin": [ { "coin": "USDC", "equity": "200.62572554", "usdValue": "200.62572554", "walletBalance": "201.34882644", "availableToWithdraw": "0", "availableToBorrow": "1500000", "borrowAmount": "0", "accruedInterest": "0", "totalOrderIM": "0", "totalPositionIM": "202.99874213", "totalPositionMM": "39.14289747", "unrealisedPnl": "74.2768991", "cumRealisedPnl": "-209.1544627", "bonus": "0" }, { "coin": "BTC", "equity": "0.06488393", "usdValue": "1023.08402268", "walletBalance": "0.06488393", "availableToWithdraw": "0.06488393", "availableToBorrow": "2.5", "borrowAmount": "0", "accruedInterest": "0", "totalOrderIM": "0", "totalPositionIM": "0", "totalPositionMM": "0", "unrealisedPnl": "0", "cumRealisedPnl": "0", "bonus": "0" }, { "coin": "ETH", "equity": "0", "usdValue": "0", "walletBalance": "0", "availableToWithdraw": "0", "availableToBorrow": "26", "borrowAmount": "0", "accruedInterest": "0", "totalOrderIM": "0", "totalPositionIM": "0", "totalPositionMM": "0", "unrealisedPnl": "0", "cumRealisedPnl": "0", "bonus": "0" }, { "coin": "USDT", "equity": "11726.64664904", "usdValue": "11613.58597018", "walletBalance": "11728.54414904", "availableToWithdraw": "11723.92075829", "availableToBorrow": "2500000", "borrowAmount": "0", "accruedInterest": "0", "totalOrderIM": "0", "totalPositionIM": "2.72589075", "totalPositionMM": "0.28576575", "unrealisedPnl": "-1.8975", "cumRealisedPnl": "0.64782276", "bonus": "0" }, { "coin": "EOS3L", "equity": "215.0570412", "usdValue": "0", "walletBalance": "215.0570412", "availableToWithdraw": "215.0570412", "availableToBorrow": "0", "borrowAmount": "0", "accruedInterest": "", "totalOrderIM": "0", "totalPositionIM": "0", "totalPositionMM": "0", "unrealisedPnl": "0", "cumRealisedPnl": "0", "bonus": "0" }, { "coin": "BIT", "equity": "1.82", "usdValue": "0.48758257", "walletBalance": "1.82", "availableToWithdraw": "1.82", "availableToBorrow": "0", "borrowAmount": "0", "accruedInterest": "", "totalOrderIM": "0", "totalPositionIM": "0", "totalPositionMM": "0", "unrealisedPnl": "0", "cumRealisedPnl": "0", "bonus": "0" } ], "accountType": "UNIFIED", "accountLTV": "0.017" } ] }
{ "id": "592324fa945a30-2603-49a5-b865-21668c29f2a6", "topic": "greeks", "creationTime": 1672364262482, "data": [ { "baseCoin": "ETH", "totalDelta": "0.06999986", "totalGamma": "-0.00000001", "totalVega": "-0.00000024", "totalTheta": "0.00001314" } ] }
{"id": "592324803b2785-26fa-4214-9963-bdd4727f07be", "topic": "execution", "creationTime": 1672364174455, "data": [ { "category": "linear", "symbol": "XRPUSDT", "execFee": "0.005061", "execId": "7e2ae69c-4edf-5800-a352-893d52b446aa", "execPrice": "0.3374", "execQty": "25", "execType": "Trade", "execValue": "8.435", "isMaker": false, "feeRate": "0.0006", "tradeIv": "", "markIv": "", "blockTradeId": "", "markPrice": "0.3391", "indexPrice": "", "underlyingPrice": "", "leavesQty": "0", "orderId": "f6e324ff-99c2-4e89-9739-3086e47f9381", "orderLinkId": "", "orderPrice": "0.3207", "orderQty":"25","orderType":"Market","stopOrderType":"UNKNOWN","side":"Sell","execTime":"1672364174443","isLeverage": "0","closedSize": "","seq":4688002127}]}
{ "id": "someID", "topic": "order", "creationTime": 1672364262474, "data": [{"category":"linear","symbol":"BTCUSDT","orderId":"c1956690-b731-4191-97c0-94b00422231b","orderLinkId":"","blockTradeId":"","side":"Sell","positionIdx":0,"orderStatus":"Filled","cancelType":"UNKNOWN","rejectReason":"EC_NoError","timeInForce":"IOC","isLeverage":"","price":"4.033","qty":"1.7","avgPrice":"4.24","leavesQty":"0","leavesValue":"0","cumExecQty":"1.7","cumExecValue":"7.2086","cumExecFee":"0.00288344","orderType":"Market","stopOrderType":"","orderIv":"","triggerPrice":"","takeProfit":"","stopLoss":"","triggerBy":"","tpTriggerBy":"","slTriggerBy":"","triggerDirection":0,"placeType":"","lastPriceOnCreated":"4.245","closeOnTrigger":false,"reduceOnly":false,"smpGroup":0,"smpType":"None","smpOrderId":"","slLimitPrice":"0","tpLimitPrice":"0","tpslMode":"UNKNOWN","createType":"CreateByUser","marketUnit":"","createdTime":"1733778525913","updatedTime":"1733778525917","feeCurrency":"","closedPnl":"0"}]}