mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-13 23:16:45 +00:00
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:
@@ -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")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
44
exchanges/bybit/inverse_websocket.go
Normal file
44
exchanges/bybit/inverse_websocket.go
Normal 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)
|
||||
}
|
||||
58
exchanges/bybit/inverse_websocket_test.go
Normal file
58
exchanges/bybit/inverse_websocket_test.go
Normal 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")
|
||||
}
|
||||
61
exchanges/bybit/linear_websocket.go
Normal file
61
exchanges/bybit/linear_websocket.go
Normal 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
|
||||
}
|
||||
78
exchanges/bybit/linear_websocket_test.go
Normal file
78
exchanges/bybit/linear_websocket_test.go
Normal 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")
|
||||
}
|
||||
44
exchanges/bybit/options_websocket.go
Normal file
44
exchanges/bybit/options_websocket.go
Normal 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)
|
||||
}
|
||||
58
exchanges/bybit/options_websocket_test.go
Normal file
58
exchanges/bybit/options_websocket_test.go
Normal 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")
|
||||
}
|
||||
50
exchanges/bybit/spot_websocket.go
Normal file
50
exchanges/bybit/spot_websocket.go
Normal 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)
|
||||
}
|
||||
30
exchanges/bybit/spot_websocket_test.go
Normal file
30
exchanges/bybit/spot_websocket_test.go
Normal 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
6
exchanges/bybit/testdata/wsAuth.json
vendored
Normal 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"}]}
|
||||
@@ -1242,8 +1242,7 @@ func (b *Base) NewEndpoints() *Endpoints {
|
||||
// SetDefaultEndpoints declares and sets the default URLs map
|
||||
func (e *Endpoints) SetDefaultEndpoints(m map[URL]string) error {
|
||||
for k, v := range m {
|
||||
err := e.SetRunningURL(k.String(), v)
|
||||
if err != nil {
|
||||
if err := e.SetRunningURL(k.String(), v); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -1380,6 +1379,16 @@ func (u URL) String() string {
|
||||
return restSwapURL
|
||||
case WebsocketSpot:
|
||||
return websocketSpotURL
|
||||
case WebsocketCoinMargined:
|
||||
return websocketCoinMarginedURL
|
||||
case WebsocketUSDTMargined:
|
||||
return websocketUSDTMarginedURL
|
||||
case WebsocketUSDCMargined:
|
||||
return websocketUSDCMarginedURL
|
||||
case WebsocketOptions:
|
||||
return websocketOptionsURL
|
||||
case WebsocketPrivate:
|
||||
return websocketPrivateURL
|
||||
case WebsocketSpotSupplementary:
|
||||
return websocketSpotSupplementaryURL
|
||||
case ChainAnalysis:
|
||||
@@ -1418,6 +1427,16 @@ func getURLTypeFromString(ep string) (URL, error) {
|
||||
return RestSwap, nil
|
||||
case websocketSpotURL:
|
||||
return WebsocketSpot, nil
|
||||
case websocketCoinMarginedURL:
|
||||
return WebsocketCoinMargined, nil
|
||||
case websocketUSDTMarginedURL:
|
||||
return WebsocketUSDTMargined, nil
|
||||
case websocketUSDCMarginedURL:
|
||||
return WebsocketUSDCMargined, nil
|
||||
case websocketOptionsURL:
|
||||
return WebsocketOptions, nil
|
||||
case websocketPrivateURL:
|
||||
return WebsocketPrivate, nil
|
||||
case websocketSpotSupplementaryURL:
|
||||
return WebsocketSpotSupplementary, nil
|
||||
case chainAnalysisURL:
|
||||
|
||||
@@ -1692,50 +1692,39 @@ func TestAddTradesToBuffer(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestString(t *testing.T) {
|
||||
if RestSpot.String() != restSpotURL {
|
||||
t.Errorf("received '%v' expected '%v'", RestSpot, restSpotURL)
|
||||
}
|
||||
if RestSpotSupplementary.String() != restSpotSupplementaryURL {
|
||||
t.Errorf("received '%v' expected '%v'", RestSpotSupplementary, restSpotSupplementaryURL)
|
||||
}
|
||||
if RestUSDTMargined.String() != "RestUSDTMarginedFuturesURL" {
|
||||
t.Errorf("received '%v' expected '%v'", RestUSDTMargined, "RestUSDTMarginedFuturesURL")
|
||||
}
|
||||
if RestCoinMargined.String() != restCoinMarginedFuturesURL {
|
||||
t.Errorf("received '%v' expected '%v'", RestCoinMargined, restCoinMarginedFuturesURL)
|
||||
}
|
||||
if RestFutures.String() != restFuturesURL {
|
||||
t.Errorf("received '%v' expected '%v'", RestFutures, restFuturesURL)
|
||||
}
|
||||
if RestFuturesSupplementary.String() != restFuturesSupplementaryURL {
|
||||
t.Errorf("received '%v' expected '%v'", RestFutures, restFuturesSupplementaryURL)
|
||||
}
|
||||
if RestUSDCMargined.String() != restUSDCMarginedFuturesURL {
|
||||
t.Errorf("received '%v' expected '%v'", RestUSDCMargined, restUSDCMarginedFuturesURL)
|
||||
}
|
||||
if RestSandbox.String() != restSandboxURL {
|
||||
t.Errorf("received '%v' expected '%v'", RestSandbox, restSandboxURL)
|
||||
}
|
||||
if RestSwap.String() != restSwapURL {
|
||||
t.Errorf("received '%v' expected '%v'", RestSwap, restSwapURL)
|
||||
}
|
||||
if WebsocketSpot.String() != websocketSpotURL {
|
||||
t.Errorf("received '%v' expected '%v'", WebsocketSpot, websocketSpotURL)
|
||||
}
|
||||
if WebsocketSpotSupplementary.String() != websocketSpotSupplementaryURL {
|
||||
t.Errorf("received '%v' expected '%v'", WebsocketSpotSupplementary, websocketSpotSupplementaryURL)
|
||||
}
|
||||
if ChainAnalysis.String() != chainAnalysisURL {
|
||||
t.Errorf("received '%v' expected '%v'", ChainAnalysis, chainAnalysisURL)
|
||||
}
|
||||
if EdgeCase1.String() != edgeCase1URL {
|
||||
t.Errorf("received '%v' expected '%v'", EdgeCase1, edgeCase1URL)
|
||||
}
|
||||
if EdgeCase2.String() != edgeCase2URL {
|
||||
t.Errorf("received '%v' expected '%v'", EdgeCase2, edgeCase2URL)
|
||||
}
|
||||
if EdgeCase3.String() != edgeCase3URL {
|
||||
t.Errorf("received '%v' expected '%v'", EdgeCase3, edgeCase3URL)
|
||||
t.Parallel()
|
||||
|
||||
for _, tc := range []struct {
|
||||
url URL
|
||||
expected string
|
||||
}{
|
||||
{0, ""},
|
||||
{RestSpot, restSpotURL},
|
||||
{RestSpotSupplementary, restSpotSupplementaryURL},
|
||||
{RestUSDTMargined, restUSDTMarginedFuturesURL},
|
||||
{RestCoinMargined, restCoinMarginedFuturesURL},
|
||||
{RestFutures, restFuturesURL},
|
||||
{RestFuturesSupplementary, restFuturesSupplementaryURL},
|
||||
{RestUSDCMargined, restUSDCMarginedFuturesURL},
|
||||
{RestSandbox, restSandboxURL},
|
||||
{RestSwap, restSwapURL},
|
||||
{WebsocketSpot, websocketSpotURL},
|
||||
{WebsocketCoinMargined, websocketCoinMarginedURL},
|
||||
{WebsocketUSDTMargined, websocketUSDTMarginedURL},
|
||||
{WebsocketUSDCMargined, websocketUSDCMarginedURL},
|
||||
{WebsocketOptions, websocketOptionsURL},
|
||||
{WebsocketPrivate, websocketPrivateURL},
|
||||
{WebsocketSpotSupplementary, websocketSpotSupplementaryURL},
|
||||
{ChainAnalysis, chainAnalysisURL},
|
||||
{EdgeCase1, edgeCase1URL},
|
||||
{EdgeCase2, edgeCase2URL},
|
||||
{EdgeCase3, edgeCase3URL},
|
||||
{420, ""},
|
||||
} {
|
||||
t.Run(tc.url.String(), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert.Equal(t, tc.expected, tc.url.String(), "String() should return the expected URL")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1886,20 +1875,26 @@ func TestGetGetURLTypeFromString(t *testing.T) {
|
||||
Expected URL
|
||||
Error error
|
||||
}{
|
||||
{Endpoint: "RestSpotURL", Expected: RestSpot},
|
||||
{Endpoint: "RestSpotSupplementaryURL", Expected: RestSpotSupplementary},
|
||||
{Endpoint: "RestUSDTMarginedFuturesURL", Expected: RestUSDTMargined},
|
||||
{Endpoint: "RestCoinMarginedFuturesURL", Expected: RestCoinMargined},
|
||||
{Endpoint: "RestFuturesURL", Expected: RestFutures},
|
||||
{Endpoint: "RestUSDCMarginedFuturesURL", Expected: RestUSDCMargined},
|
||||
{Endpoint: "RestSandboxURL", Expected: RestSandbox},
|
||||
{Endpoint: "RestSwapURL", Expected: RestSwap},
|
||||
{Endpoint: "WebsocketSpotURL", Expected: WebsocketSpot},
|
||||
{Endpoint: "WebsocketSpotSupplementaryURL", Expected: WebsocketSpotSupplementary},
|
||||
{Endpoint: "ChainAnalysisURL", Expected: ChainAnalysis},
|
||||
{Endpoint: "EdgeCase1URL", Expected: EdgeCase1},
|
||||
{Endpoint: "EdgeCase2URL", Expected: EdgeCase2},
|
||||
{Endpoint: "EdgeCase3URL", Expected: EdgeCase3},
|
||||
{Endpoint: restSpotURL, Expected: RestSpot},
|
||||
{Endpoint: restSpotSupplementaryURL, Expected: RestSpotSupplementary},
|
||||
{Endpoint: restUSDTMarginedFuturesURL, Expected: RestUSDTMargined},
|
||||
{Endpoint: restCoinMarginedFuturesURL, Expected: RestCoinMargined},
|
||||
{Endpoint: restFuturesURL, Expected: RestFutures},
|
||||
{Endpoint: restFuturesSupplementaryURL, Expected: RestFuturesSupplementary},
|
||||
{Endpoint: restUSDCMarginedFuturesURL, Expected: RestUSDCMargined},
|
||||
{Endpoint: restSandboxURL, Expected: RestSandbox},
|
||||
{Endpoint: restSwapURL, Expected: RestSwap},
|
||||
{Endpoint: websocketSpotURL, Expected: WebsocketSpot},
|
||||
{Endpoint: websocketCoinMarginedURL, Expected: WebsocketCoinMargined},
|
||||
{Endpoint: websocketUSDTMarginedURL, Expected: WebsocketUSDTMargined},
|
||||
{Endpoint: websocketUSDCMarginedURL, Expected: WebsocketUSDCMargined},
|
||||
{Endpoint: websocketOptionsURL, Expected: WebsocketOptions},
|
||||
{Endpoint: websocketPrivateURL, Expected: WebsocketPrivate},
|
||||
{Endpoint: websocketSpotSupplementaryURL, Expected: WebsocketSpotSupplementary},
|
||||
{Endpoint: chainAnalysisURL, Expected: ChainAnalysis},
|
||||
{Endpoint: edgeCase1URL, Expected: EdgeCase1},
|
||||
{Endpoint: edgeCase2URL, Expected: EdgeCase2},
|
||||
{Endpoint: edgeCase3URL, Expected: EdgeCase3},
|
||||
{Endpoint: "sillyMcSillyBilly", Expected: 0, Error: errEndpointStringNotFound},
|
||||
}
|
||||
|
||||
|
||||
@@ -273,6 +273,11 @@ const (
|
||||
RestSwap
|
||||
RestSandbox
|
||||
WebsocketSpot
|
||||
WebsocketCoinMargined
|
||||
WebsocketUSDTMargined
|
||||
WebsocketUSDCMargined
|
||||
WebsocketOptions
|
||||
WebsocketPrivate
|
||||
WebsocketSpotSupplementary
|
||||
ChainAnalysis
|
||||
EdgeCase1
|
||||
@@ -289,6 +294,11 @@ const (
|
||||
restSandboxURL = "RestSandboxURL"
|
||||
restSwapURL = "RestSwapURL"
|
||||
websocketSpotURL = "WebsocketSpotURL"
|
||||
websocketCoinMarginedURL = "WebsocketCoinMarginedURL"
|
||||
websocketUSDTMarginedURL = "WebsocketUSDTMarginedURL"
|
||||
websocketUSDCMarginedURL = "WebsocketUSDCMarginedURL"
|
||||
websocketOptionsURL = "WebsocketOptionsURL"
|
||||
websocketPrivateURL = "WebsocketPrivateURL"
|
||||
websocketSpotSupplementaryURL = "WebsocketSpotSupplementaryURL"
|
||||
chainAnalysisURL = "ChainAnalysisURL"
|
||||
edgeCase1URL = "EdgeCase1URL"
|
||||
@@ -307,6 +317,11 @@ var keyURLs = []URL{
|
||||
RestSwap,
|
||||
RestSandbox,
|
||||
WebsocketSpot,
|
||||
WebsocketCoinMargined,
|
||||
WebsocketUSDTMargined,
|
||||
WebsocketUSDCMargined,
|
||||
WebsocketOptions,
|
||||
WebsocketPrivate,
|
||||
WebsocketSpotSupplementary,
|
||||
ChainAnalysis,
|
||||
EdgeCase1,
|
||||
|
||||
@@ -188,8 +188,9 @@ func timeInForceFromString(tif string) (order.TimeInForce, error) {
|
||||
|
||||
// Exchange implements exchange.IBotExchange and contains additional specific api methods for interacting with GateIO
|
||||
type Exchange struct {
|
||||
Counter common.Counter // Must be first due to alignment requirements
|
||||
exchange.Base
|
||||
|
||||
messageIDSeq common.Counter
|
||||
wsOBUpdateMgr *wsOBUpdateManager
|
||||
}
|
||||
|
||||
|
||||
@@ -2324,7 +2324,7 @@ func TestSubscribe(t *testing.T) {
|
||||
subs, err := e.Features.Subscriptions.ExpandTemplates(e)
|
||||
require.NoError(t, err, "ExpandTemplates must not error")
|
||||
e.Features.Subscriptions = subscription.List{}
|
||||
err = e.Subscribe(t.Context(), &DummyConnection{}, subs)
|
||||
err = e.Subscribe(t.Context(), &FixtureConnection{}, subs)
|
||||
require.NoError(t, err, "Subscribe must not error")
|
||||
}
|
||||
|
||||
@@ -2866,15 +2866,10 @@ func TestGetSettlementCurrency(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateWebsocketMessageID(t *testing.T) {
|
||||
t.Parallel()
|
||||
require.NotEmpty(t, e.GenerateWebsocketMessageID(false))
|
||||
}
|
||||
type FixtureConnection struct{ websocket.Connection }
|
||||
|
||||
type DummyConnection struct{ websocket.Connection }
|
||||
|
||||
func (d *DummyConnection) GenerateMessageID(bool) int64 { return 1337 }
|
||||
func (d *DummyConnection) SendMessageReturnResponse(context.Context, request.EndpointLimit, any, any) ([]byte, error) {
|
||||
func (d *FixtureConnection) GenerateMessageID(bool) int64 { return 1337 }
|
||||
func (d *FixtureConnection) SendMessageReturnResponse(context.Context, request.EndpointLimit, any, any) ([]byte, error) {
|
||||
return []byte(`{"time":1726121320,"time_ms":1726121320745,"id":1,"conn_id":"f903779a148987ca","trace_id":"d8ee37cd14347e4ed298d44e69aedaa7","channel":"spot.tickers","event":"subscribe","payload":["BRETT_USDT"],"result":{"status":"success"},"requestId":"d8ee37cd14347e4ed298d44e69aedaa7"}`), nil
|
||||
}
|
||||
|
||||
@@ -2883,12 +2878,12 @@ func TestHandleSubscriptions(t *testing.T) {
|
||||
|
||||
subs := subscription.List{{Channel: subscription.OrderbookChannel}}
|
||||
|
||||
err := e.handleSubscription(t.Context(), &DummyConnection{}, subscribeEvent, subs, func(context.Context, websocket.Connection, string, subscription.List) ([]WsInput, error) {
|
||||
err := e.handleSubscription(t.Context(), &FixtureConnection{}, subscribeEvent, subs, func(context.Context, websocket.Connection, string, subscription.List) ([]WsInput, error) {
|
||||
return []WsInput{{}}, nil
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = e.handleSubscription(t.Context(), &DummyConnection{}, unsubscribeEvent, subs, func(context.Context, websocket.Connection, string, subscription.List) ([]WsInput, error) {
|
||||
err = e.handleSubscription(t.Context(), &FixtureConnection{}, unsubscribeEvent, subs, func(context.Context, websocket.Connection, string, subscription.List) ([]WsInput, error) {
|
||||
return []WsInput{{}}, nil
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -729,11 +729,6 @@ func (e *Exchange) Unsubscribe(ctx context.Context, conn websocket.Connection, s
|
||||
return e.manageSubs(ctx, unsubscribeEvent, conn, subs)
|
||||
}
|
||||
|
||||
// GenerateWebsocketMessageID generates a message ID for the individual connection
|
||||
func (e *Exchange) GenerateWebsocketMessageID(bool) int64 {
|
||||
return e.Counter.IncrementAndGet()
|
||||
}
|
||||
|
||||
// channelName converts global channel names to gateio specific channel names
|
||||
func channelName(s *subscription.Subscription) string {
|
||||
if name, ok := subscriptionNames[s.Channel]; ok {
|
||||
|
||||
@@ -46,7 +46,7 @@ func (e *Exchange) WebsocketSpotSubmitOrders(ctx context.Context, orders ...*Cre
|
||||
for i := range orders {
|
||||
if orders[i].Text == "" {
|
||||
// API requires Text field, or it will be rejected
|
||||
orders[i].Text = "t-" + strconv.FormatInt(e.Counter.IncrementAndGet(), 10)
|
||||
orders[i].Text = "t-" + strconv.FormatInt(e.messageIDSeq.IncrementAndGet(), 10)
|
||||
}
|
||||
if orders[i].CurrencyPair.IsEmpty() {
|
||||
return nil, currency.ErrCurrencyPairEmpty
|
||||
|
||||
@@ -208,17 +208,17 @@ func (e *Exchange) Setup(exch *config.Exchange) error {
|
||||
}
|
||||
// Spot connection
|
||||
err = e.Websocket.SetupNewConnection(&websocket.ConnectionSetup{
|
||||
URL: gateioWebsocketEndpoint,
|
||||
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
|
||||
ResponseMaxLimit: exch.WebsocketResponseMaxLimit,
|
||||
Handler: e.WsHandleSpotData,
|
||||
Subscriber: e.Subscribe,
|
||||
Unsubscriber: e.Unsubscribe,
|
||||
GenerateSubscriptions: e.generateSubscriptionsSpot,
|
||||
Connector: e.WsConnectSpot,
|
||||
Authenticate: e.authenticateSpot,
|
||||
MessageFilter: asset.Spot,
|
||||
BespokeGenerateMessageID: e.GenerateWebsocketMessageID,
|
||||
URL: gateioWebsocketEndpoint,
|
||||
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
|
||||
ResponseMaxLimit: exch.WebsocketResponseMaxLimit,
|
||||
Handler: e.WsHandleSpotData,
|
||||
Subscriber: e.Subscribe,
|
||||
Unsubscriber: e.Unsubscribe,
|
||||
GenerateSubscriptions: e.generateSubscriptionsSpot,
|
||||
Connector: e.WsConnectSpot,
|
||||
Authenticate: e.authenticateSpot,
|
||||
MessageFilter: asset.Spot,
|
||||
RequestIDGenerator: e.messageIDSeq.IncrementAndGet,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -236,10 +236,10 @@ func (e *Exchange) Setup(exch *config.Exchange) error {
|
||||
GenerateSubscriptions: func() (subscription.List, error) {
|
||||
return e.GenerateFuturesDefaultSubscriptions(asset.USDTMarginedFutures)
|
||||
},
|
||||
Connector: e.WsFuturesConnect,
|
||||
Authenticate: e.authenticateFutures,
|
||||
MessageFilter: asset.USDTMarginedFutures,
|
||||
BespokeGenerateMessageID: e.GenerateWebsocketMessageID,
|
||||
Connector: e.WsFuturesConnect,
|
||||
Authenticate: e.authenticateFutures,
|
||||
MessageFilter: asset.USDTMarginedFutures,
|
||||
RequestIDGenerator: e.messageIDSeq.IncrementAndGet,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -258,9 +258,9 @@ func (e *Exchange) Setup(exch *config.Exchange) error {
|
||||
GenerateSubscriptions: func() (subscription.List, error) {
|
||||
return e.GenerateFuturesDefaultSubscriptions(asset.CoinMarginedFutures)
|
||||
},
|
||||
Connector: e.WsFuturesConnect,
|
||||
MessageFilter: asset.CoinMarginedFutures,
|
||||
BespokeGenerateMessageID: e.GenerateWebsocketMessageID,
|
||||
Connector: e.WsFuturesConnect,
|
||||
MessageFilter: asset.CoinMarginedFutures,
|
||||
RequestIDGenerator: e.messageIDSeq.IncrementAndGet,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -275,12 +275,12 @@ func (e *Exchange) Setup(exch *config.Exchange) error {
|
||||
Handler: func(ctx context.Context, conn websocket.Connection, incoming []byte) error {
|
||||
return e.WsHandleFuturesData(ctx, conn, incoming, asset.DeliveryFutures)
|
||||
},
|
||||
Subscriber: e.DeliveryFuturesSubscribe,
|
||||
Unsubscriber: e.DeliveryFuturesUnsubscribe,
|
||||
GenerateSubscriptions: e.GenerateDeliveryFuturesDefaultSubscriptions,
|
||||
Connector: e.WsDeliveryFuturesConnect,
|
||||
MessageFilter: asset.DeliveryFutures,
|
||||
BespokeGenerateMessageID: e.GenerateWebsocketMessageID,
|
||||
Subscriber: e.DeliveryFuturesSubscribe,
|
||||
Unsubscriber: e.DeliveryFuturesUnsubscribe,
|
||||
GenerateSubscriptions: e.GenerateDeliveryFuturesDefaultSubscriptions,
|
||||
Connector: e.WsDeliveryFuturesConnect,
|
||||
MessageFilter: asset.DeliveryFutures,
|
||||
RequestIDGenerator: e.messageIDSeq.IncrementAndGet,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -288,16 +288,16 @@ func (e *Exchange) Setup(exch *config.Exchange) error {
|
||||
|
||||
// Futures connection - Options
|
||||
return e.Websocket.SetupNewConnection(&websocket.ConnectionSetup{
|
||||
URL: optionsWebsocketURL,
|
||||
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
|
||||
ResponseMaxLimit: exch.WebsocketResponseMaxLimit,
|
||||
Handler: e.WsHandleOptionsData,
|
||||
Subscriber: e.OptionsSubscribe,
|
||||
Unsubscriber: e.OptionsUnsubscribe,
|
||||
GenerateSubscriptions: e.GenerateOptionsDefaultSubscriptions,
|
||||
Connector: e.WsOptionsConnect,
|
||||
MessageFilter: asset.Options,
|
||||
BespokeGenerateMessageID: e.GenerateWebsocketMessageID,
|
||||
URL: optionsWebsocketURL,
|
||||
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
|
||||
ResponseMaxLimit: exch.WebsocketResponseMaxLimit,
|
||||
Handler: e.WsHandleOptionsData,
|
||||
Subscriber: e.OptionsSubscribe,
|
||||
Unsubscriber: e.OptionsUnsubscribe,
|
||||
GenerateSubscriptions: e.GenerateOptionsDefaultSubscriptions,
|
||||
Connector: e.WsOptionsConnect,
|
||||
MessageFilter: asset.Options,
|
||||
RequestIDGenerator: e.messageIDSeq.IncrementAndGet,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -219,22 +219,22 @@ func (e *Exchange) Setup(exch *config.Exchange) error {
|
||||
}
|
||||
|
||||
if err := e.Websocket.SetupNewConnection(&websocket.ConnectionSetup{
|
||||
URL: apiWebsocketPublicURL,
|
||||
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
|
||||
ResponseMaxLimit: websocketResponseMaxLimit,
|
||||
RateLimit: request.NewRateLimitWithWeight(time.Second, 2, 1),
|
||||
BespokeGenerateMessageID: func(bool) int64 { return e.messageIDSeq.IncrementAndGet() },
|
||||
URL: apiWebsocketPublicURL,
|
||||
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
|
||||
ResponseMaxLimit: websocketResponseMaxLimit,
|
||||
RateLimit: request.NewRateLimitWithWeight(time.Second, 2, 1),
|
||||
RequestIDGenerator: e.messageIDSeq.IncrementAndGet,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return e.Websocket.SetupNewConnection(&websocket.ConnectionSetup{
|
||||
URL: apiWebsocketPrivateURL,
|
||||
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
|
||||
ResponseMaxLimit: websocketResponseMaxLimit,
|
||||
Authenticated: true,
|
||||
RateLimit: request.NewRateLimitWithWeight(time.Second, 2, 1),
|
||||
BespokeGenerateMessageID: func(bool) int64 { return e.messageIDSeq.IncrementAndGet() },
|
||||
URL: apiWebsocketPrivateURL,
|
||||
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
|
||||
ResponseMaxLimit: websocketResponseMaxLimit,
|
||||
Authenticated: true,
|
||||
RateLimit: request.NewRateLimitWithWeight(time.Second, 2, 1),
|
||||
RequestIDGenerator: e.messageIDSeq.IncrementAndGet,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user