mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-13 15:09:42 +00:00
bybit: Add websocket trading functionality across all assets (#1672)
* 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 * bybit: Add websocket trading functionality across all assets * rm json parser and handle in json package instead * in favour of json package unmarshalling * Add bool ConnectionDoesNotRequireSubscriptions so that we don't need to handle dummy sub * handle pong response * spelling * linter: fix * fix processing issues with tickers * 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 * rm comment as it does not * update time fields for orderbook latency * fix time conversion * Add func MatchReturnResponses * glorious: nits and stuff * lint: fix * attempt to fix race * linter: fix * fix tests * types/time: strict usage of time type for usage with unix timestamps * fix tests etc * Allow match back with order details * Add time in force values for different order types + extra return information on websocket trading * 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 * bybit/websocket: allow a shared ID between outbound payloads for inbound matching * 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 * linter: fix * misc/linter: fix * change function names * 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 * websocket: skip connections with subscriptions not required during channel flush * websocket: simplify error handling in FlushChannels using if short * 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 * fix comments * bybit: fix merge regression on websocket message filter * cranktakular: nits * bybit: Add global rate limits for websocket * ai: nits * linter: fix * 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> * bybit: use connection method for segregated match on multi-connection * cleanup after master merge * fix test and config whoops * cranktakular: nits * exchange: add missing tests for base method websocket order funcs * cranktakular: nits and refresh + tests * cranktakular: pedantic nits * linter: fixes * t.Parallel tests * glorious nit * Update exchange/websocket/connection.go Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com> * gk: nits * boss king: nits * canktakular: nits * Update exchanges/bybit/bybit_websocket.go Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com> * Update exchanges/bybit/bybit_websocket_requests.go Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com> * Update exchanges/bybit/bybit_websocket_requests.go Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com> * Update exchanges/bybit/bybit_websocket_requests.go Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com> * Update exchanges/bybit/bybit_websocket_requests.go Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com> * gk: nits * linter: fix * Update exchanges/bybit/bybit.go Co-authored-by: Samuael A. <39623015+samuael@users.noreply.github.com> * Update exchanges/bybit/bybit.go Co-authored-by: Samuael A. <39623015+samuael@users.noreply.github.com> * bossking: nits * gk: much nicer design * gk: revised naming for consideration * gk: nits * gk: nits restrict in configtest.json and not worry about many pairs enabled * rm log * linter: fix * codex: nit * cranktakular: nits * Update exchanges/bybit/bybit_websocket_requests.go Co-authored-by: Scott <gloriousCode@users.noreply.github.com> * Update exchanges/bybit/bybit_websocket_requests.go Co-authored-by: Scott <gloriousCode@users.noreply.github.com> * Update exchanges/bybit/bybit_wrapper.go Co-authored-by: Scott <gloriousCode@users.noreply.github.com> * glorious: nits! * thrasher: nits --------- Co-authored-by: shazbert <ryan.oharareid@thrasher.io> Co-authored-by: Scott <gloriousCode@users.noreply.github.com> Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com> Co-authored-by: Adrian Gallagher <adrian.gallagher@thrasher.io> Co-authored-by: Samuael A. <39623015+samuael@users.noreply.github.com>
This commit is contained in:
@@ -328,7 +328,7 @@ func (m *WebsocketRoutineManager) websocketDataHandler(exchName string, data any
|
||||
case order.ClassificationError:
|
||||
return fmt.Errorf("%w %s", d.Err, d.Error())
|
||||
case websocket.UnhandledMessageWarning:
|
||||
log.Warnln(log.WebsocketMgr, d.Message)
|
||||
log.Warnf(log.WebsocketMgr, "%s unhandled message - %s", exchName, d.Message)
|
||||
case account.Change:
|
||||
if m.verbose {
|
||||
m.printAccountHoldingsChangeSummary(exchName, d)
|
||||
|
||||
@@ -57,15 +57,20 @@ type Connection interface {
|
||||
Shutdown() error
|
||||
// RequireMatchWithData routes incoming data using the connection specific match system to the correct handler
|
||||
RequireMatchWithData(signature any, incoming []byte) error
|
||||
// IncomingWithData routes incoming data using the connection specific match system to the correct handler
|
||||
IncomingWithData(signature any, data []byte) bool
|
||||
// MatchReturnResponses sets up a channel to listen for an expected number of responses.
|
||||
MatchReturnResponses(ctx context.Context, signature any, expected int) (<-chan MatchedResponse, error)
|
||||
}
|
||||
|
||||
// ConnectionSetup defines variables for an individual stream connection
|
||||
type ConnectionSetup struct {
|
||||
ResponseCheckTimeout time.Duration
|
||||
ResponseMaxLimit time.Duration
|
||||
RateLimit *request.RateLimiterWithWeight
|
||||
Authenticated bool
|
||||
ConnectionLevelReporter Reporter
|
||||
ResponseCheckTimeout time.Duration
|
||||
ResponseMaxLimit time.Duration
|
||||
RateLimit *request.RateLimiterWithWeight
|
||||
Authenticated bool // unused for multi-connection websocket
|
||||
SubscriptionsNotRequired bool
|
||||
ConnectionLevelReporter Reporter
|
||||
|
||||
// URL defines the websocket server URL to connect to
|
||||
URL string
|
||||
@@ -92,7 +97,8 @@ type ConnectionSetup struct {
|
||||
// This is useful for when an exchange connection requires a unique or
|
||||
// structured message ID for each message sent.
|
||||
RequestIDGenerator func() int64
|
||||
Authenticate func(ctx context.Context, conn Connection) error
|
||||
// Authenticate will be called to authenticate the connection
|
||||
Authenticate func(ctx context.Context, conn Connection) error
|
||||
// MessageFilter defines the criteria used to match messages to a specific connection.
|
||||
// The filter enables precise routing and handling of messages for distinct connection contexts.
|
||||
MessageFilter any
|
||||
@@ -467,6 +473,30 @@ inspection:
|
||||
return resps, nil
|
||||
}
|
||||
|
||||
// MatchedResponse encapsulates the matched responses along with any errors encountered.
|
||||
type MatchedResponse struct {
|
||||
Responses [][]byte
|
||||
Err error
|
||||
}
|
||||
|
||||
// MatchReturnResponses returns channel of exactly expected matched responses
|
||||
func (c *connection) MatchReturnResponses(ctx context.Context, signature any, expected int) (<-chan MatchedResponse, error) {
|
||||
connectionListen, err := c.Match.Set(signature, expected)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out := make(chan MatchedResponse, 1) // buffered so routine below doesn't block if no receiver
|
||||
|
||||
go func() {
|
||||
resps, err := c.waitForResponses(ctx, signature, connectionListen, expected, nil)
|
||||
out <- MatchedResponse{Responses: resps, Err: err}
|
||||
close(out)
|
||||
}()
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func removeURLQueryString(u string) string {
|
||||
if index := strings.Index(u, "?"); index != -1 {
|
||||
return u[:index]
|
||||
@@ -478,3 +508,8 @@ func removeURLQueryString(u string) string {
|
||||
func (c *connection) RequireMatchWithData(signature any, incoming []byte) error {
|
||||
return c.Match.RequireMatchWithData(signature, incoming)
|
||||
}
|
||||
|
||||
// IncomingWithData routes incoming data using the connection specific match system to the correct handler
|
||||
func (c *connection) IncomingWithData(signature any, data []byte) bool {
|
||||
return c.Match.IncomingWithData(signature, data)
|
||||
}
|
||||
|
||||
58
exchange/websocket/connection_test.go
Normal file
58
exchange/websocket/connection_test.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMatchReturnResponses(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
conn := connection{Match: NewMatch()}
|
||||
_, err := conn.MatchReturnResponses(t.Context(), nil, 0)
|
||||
require.ErrorIs(t, err, errInvalidBufferSize)
|
||||
|
||||
ch, err := conn.MatchReturnResponses(t.Context(), nil, 1)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.ErrorIs(t, (<-ch).Err, ErrSignatureTimeout)
|
||||
conn.ResponseMaxLimit = time.Millisecond
|
||||
|
||||
ch, err = conn.MatchReturnResponses(t.Context(), nil, 1)
|
||||
require.NoError(t, err)
|
||||
|
||||
exp := []byte("test")
|
||||
require.True(t, conn.Match.IncomingWithData(nil, exp))
|
||||
assert.Equal(t, exp, (<-ch).Responses[0])
|
||||
}
|
||||
|
||||
func TestWebsocketConnectionRequireMatchWithData(t *testing.T) {
|
||||
t.Parallel()
|
||||
ws := connection{Match: NewMatch()}
|
||||
err := ws.RequireMatchWithData(0, nil)
|
||||
require.ErrorIs(t, err, ErrSignatureNotMatched)
|
||||
|
||||
ch, err := ws.Match.Set(0, 1)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ws.RequireMatchWithData(0, []byte("test"))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, ch, 1, "must have one item in channel")
|
||||
assert.Equal(t, []byte("test"), <-ch)
|
||||
}
|
||||
|
||||
func TestIncomingWithData(t *testing.T) {
|
||||
t.Parallel()
|
||||
ws := connection{Match: NewMatch()}
|
||||
require.False(t, ws.IncomingWithData(0, nil))
|
||||
|
||||
ch, err := ws.Match.Set(0, 1)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.True(t, ws.IncomingWithData(0, []byte("test")))
|
||||
require.Len(t, ch, 1, "must have one item in channel")
|
||||
assert.Equal(t, []byte("test"), <-ch)
|
||||
}
|
||||
@@ -334,13 +334,13 @@ func (m *Manager) SetupNewConnection(c *ConnectionSetup) error {
|
||||
if c.Connector == nil {
|
||||
return fmt.Errorf("%w: %w", errConnSetup, errWebsocketConnectorUnset)
|
||||
}
|
||||
if c.GenerateSubscriptions == nil {
|
||||
if c.GenerateSubscriptions == nil && !c.SubscriptionsNotRequired {
|
||||
return fmt.Errorf("%w: %w", errConnSetup, errWebsocketSubscriptionsGeneratorUnset)
|
||||
}
|
||||
if c.Subscriber == nil {
|
||||
if c.Subscriber == nil && !c.SubscriptionsNotRequired {
|
||||
return fmt.Errorf("%w: %w", errConnSetup, errWebsocketSubscriberUnset)
|
||||
}
|
||||
if c.Unsubscriber == nil && m.features.Unsubscribe {
|
||||
if c.Unsubscriber == nil && m.features.Unsubscribe && !c.SubscriptionsNotRequired {
|
||||
return fmt.Errorf("%w: %w", errConnSetup, errWebsocketUnsubscriberUnset)
|
||||
}
|
||||
if c.Handler == nil {
|
||||
@@ -482,23 +482,27 @@ func (m *Manager) connect() error {
|
||||
|
||||
// TODO: Implement concurrency below.
|
||||
for i := range m.connectionManager {
|
||||
if m.connectionManager[i].setup.GenerateSubscriptions == nil {
|
||||
multiConnectFatalError = fmt.Errorf("cannot connect to [conn:%d] [URL:%s]: %w ", i+1, m.connectionManager[i].setup.URL, errWebsocketSubscriptionsGeneratorUnset)
|
||||
break
|
||||
}
|
||||
|
||||
subs, err := m.connectionManager[i].setup.GenerateSubscriptions() // regenerate state on new connection
|
||||
if err != nil {
|
||||
multiConnectFatalError = fmt.Errorf("%s websocket: %w", m.exchangeName, common.AppendError(ErrSubscriptionFailure, err))
|
||||
break
|
||||
}
|
||||
|
||||
if len(subs) == 0 {
|
||||
// If no subscriptions are generated, we skip the connection
|
||||
if m.verbose {
|
||||
log.Warnf(log.WebsocketMgr, "%s websocket: no subscriptions generated", m.exchangeName)
|
||||
var subs subscription.List
|
||||
if !m.connectionManager[i].setup.SubscriptionsNotRequired {
|
||||
if m.connectionManager[i].setup.GenerateSubscriptions == nil {
|
||||
multiConnectFatalError = fmt.Errorf("cannot connect to [conn:%d] [URL:%s]: %w ", i+1, m.connectionManager[i].setup.URL, errWebsocketSubscriptionsGeneratorUnset)
|
||||
break
|
||||
}
|
||||
|
||||
var err error
|
||||
subs, err = m.connectionManager[i].setup.GenerateSubscriptions() // regenerate state on new connection
|
||||
if err != nil {
|
||||
multiConnectFatalError = fmt.Errorf("%s websocket: %w", m.exchangeName, common.AppendError(ErrSubscriptionFailure, err))
|
||||
break
|
||||
}
|
||||
|
||||
if len(subs) == 0 {
|
||||
// If no subscriptions are generated, we skip the connection
|
||||
if m.verbose {
|
||||
log.Warnf(log.WebsocketMgr, "%s websocket: no subscriptions generated", m.exchangeName)
|
||||
}
|
||||
continue
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if m.connectionManager[i].setup.Connector == nil {
|
||||
@@ -509,7 +513,7 @@ func (m *Manager) connect() error {
|
||||
multiConnectFatalError = fmt.Errorf("cannot connect to [conn:%d] [URL:%s]: %w ", i+1, m.connectionManager[i].setup.URL, errWebsocketDataHandlerUnset)
|
||||
break
|
||||
}
|
||||
if m.connectionManager[i].setup.Subscriber == nil {
|
||||
if m.connectionManager[i].setup.Subscriber == nil && !m.connectionManager[i].setup.SubscriptionsNotRequired {
|
||||
multiConnectFatalError = fmt.Errorf("cannot connect to [conn:%d] [URL:%s]: %w ", i+1, m.connectionManager[i].setup.URL, errWebsocketSubscriberUnset)
|
||||
break
|
||||
}
|
||||
@@ -518,8 +522,7 @@ func (m *Manager) connect() error {
|
||||
|
||||
conn := m.getConnectionFromSetup(m.connectionManager[i].setup)
|
||||
|
||||
err = m.connectionManager[i].setup.Connector(context.TODO(), conn)
|
||||
if err != nil {
|
||||
if err := m.connectionManager[i].setup.Connector(context.TODO(), conn); err != nil {
|
||||
multiConnectFatalError = fmt.Errorf("%v Error connecting %w", m.exchangeName, err)
|
||||
break
|
||||
}
|
||||
@@ -536,15 +539,17 @@ func (m *Manager) connect() error {
|
||||
go m.Reader(context.TODO(), conn, m.connectionManager[i].setup.Handler)
|
||||
|
||||
if m.connectionManager[i].setup.Authenticate != nil && m.CanUseAuthenticatedEndpoints() {
|
||||
err = m.connectionManager[i].setup.Authenticate(context.TODO(), conn)
|
||||
if err != nil {
|
||||
if err := m.connectionManager[i].setup.Authenticate(context.TODO(), conn); err != nil {
|
||||
multiConnectFatalError = fmt.Errorf("%s websocket: [conn:%d] [URL:%s] failed to authenticate %w", m.exchangeName, i+1, conn.URL, err)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
err = m.connectionManager[i].setup.Subscriber(context.TODO(), conn, subs)
|
||||
if err != nil {
|
||||
if m.connectionManager[i].setup.SubscriptionsNotRequired {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := m.connectionManager[i].setup.Subscriber(context.TODO(), conn, subs); err != nil {
|
||||
subscriptionError = common.AppendError(subscriptionError, fmt.Errorf("%v Error subscribing %w", m.exchangeName, err))
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -1336,18 +1336,3 @@ func TestGetConnection(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
assert.Same(t, expected, conn)
|
||||
}
|
||||
|
||||
func TestWebsocketConnectionRequireMatchWithData(t *testing.T) {
|
||||
t.Parallel()
|
||||
ws := connection{Match: NewMatch()}
|
||||
err := ws.RequireMatchWithData(0, nil)
|
||||
require.ErrorIs(t, err, ErrSignatureNotMatched)
|
||||
|
||||
ch, err := ws.Match.Set(0, 1)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ws.RequireMatchWithData(0, []byte("test"))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, ch, 1, "must have one item in channel")
|
||||
assert.Equal(t, []byte("test"), <-ch)
|
||||
}
|
||||
|
||||
@@ -284,6 +284,10 @@ func (m *Manager) FlushChannels() error {
|
||||
}
|
||||
|
||||
for x := range m.connectionManager {
|
||||
if m.connectionManager[x].setup.SubscriptionsNotRequired {
|
||||
continue
|
||||
}
|
||||
|
||||
newSubs, err := m.connectionManager[x].setup.GenerateSubscriptions()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -306,8 +310,7 @@ func (m *Manager) FlushChannels() error {
|
||||
m.connectionManager[x].connection = conn
|
||||
}
|
||||
|
||||
err = m.updateChannelSubscriptions(m.connectionManager[x].connection, m.connectionManager[x].subscriptions, newSubs)
|
||||
if err != nil {
|
||||
if err := m.updateChannelSubscriptions(m.connectionManager[x].connection, m.connectionManager[x].subscriptions, newSubs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -96,6 +96,7 @@ var (
|
||||
var (
|
||||
intervalMap = map[kline.Interval]string{kline.OneMin: "1", kline.ThreeMin: "3", kline.FiveMin: "5", kline.FifteenMin: "15", kline.ThirtyMin: "30", kline.OneHour: "60", kline.TwoHour: "120", kline.FourHour: "240", kline.SixHour: "360", kline.SevenHour: "720", kline.OneDay: "D", kline.OneWeek: "W", kline.OneMonth: "M"}
|
||||
stringToIntervalMap = map[string]kline.Interval{"1": kline.OneMin, "3": kline.ThreeMin, "5": kline.FiveMin, "15": kline.FifteenMin, "30": kline.ThirtyMin, "60": kline.OneHour, "120": kline.TwoHour, "240": kline.FourHour, "360": kline.SixHour, "720": kline.SevenHour, "D": kline.OneDay, "W": kline.OneWeek, "M": kline.OneMonth}
|
||||
validCategory = []string{cSpot, cLinear, cOption, cInverse}
|
||||
)
|
||||
|
||||
func intervalToString(interval kline.Interval) (string, error) {
|
||||
@@ -250,7 +251,23 @@ func (e *Exchange) GetOrderBook(ctx context.Context, category, symbol string, li
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return constructOrderbook(&resp)
|
||||
|
||||
return &Orderbook{
|
||||
Symbol: resp.Symbol,
|
||||
UpdateID: resp.UpdateID,
|
||||
GenerationTime: resp.Timestamp.Time(),
|
||||
Bids: processOB(resp.Bids),
|
||||
Asks: processOB(resp.Asks),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func processOB(ob [][2]types.Number) []orderbook.Level {
|
||||
o := make([]orderbook.Level, len(ob))
|
||||
for x := range ob {
|
||||
o[x].Price = ob[x][0].Float64()
|
||||
o[x].Amount = ob[x][1].Float64()
|
||||
}
|
||||
return o
|
||||
}
|
||||
|
||||
func fillCategoryAndSymbol(category, symbol string, optionalSymbol ...bool) (url.Values, error) {
|
||||
@@ -465,109 +482,37 @@ func isValidCategory(category string) error {
|
||||
}
|
||||
|
||||
// PlaceOrder creates an order for spot, spot margin, USDT perpetual, USDC perpetual, USDC futures, inverse futures and options.
|
||||
func (e *Exchange) PlaceOrder(ctx context.Context, arg *PlaceOrderParams) (*OrderResponse, error) {
|
||||
if arg == nil {
|
||||
return nil, errNilArgument
|
||||
}
|
||||
err := isValidCategory(arg.Category)
|
||||
if err != nil {
|
||||
func (e *Exchange) PlaceOrder(ctx context.Context, arg *PlaceOrderRequest) (*OrderResponse, error) {
|
||||
if err := arg.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if arg.Symbol.IsEmpty() {
|
||||
return nil, currency.ErrCurrencyPairEmpty
|
||||
}
|
||||
if arg.WhetherToBorrow {
|
||||
arg.IsLeverage = 1
|
||||
}
|
||||
// specifies whether to borrow or to trade.
|
||||
if arg.IsLeverage != 0 && arg.IsLeverage != 1 {
|
||||
return nil, errors.New("please provide a valid isLeverage value; must be 0 for unified spot and 1 for margin trading")
|
||||
}
|
||||
if arg.Side == "" {
|
||||
return nil, order.ErrSideIsInvalid
|
||||
}
|
||||
if arg.OrderType == "" { // Market and Limit order types are allowed
|
||||
return nil, order.ErrTypeIsInvalid
|
||||
}
|
||||
if arg.OrderQuantity <= 0 {
|
||||
return nil, limits.ErrAmountBelowMin
|
||||
}
|
||||
switch arg.TriggerDirection {
|
||||
case 0, 1, 2: // 0: None, 1: triggered when market price rises to triggerPrice, 2: triggered when market price falls to triggerPrice
|
||||
default:
|
||||
return nil, fmt.Errorf("%w, triggerDirection: %d", errInvalidTriggerDirection, arg.TriggerDirection)
|
||||
}
|
||||
if arg.OrderFilter != "" && arg.Category == cSpot {
|
||||
switch arg.OrderFilter {
|
||||
case "Order", "tpslOrder", "StopOrder":
|
||||
default:
|
||||
return nil, fmt.Errorf("%w, orderFilter=%s", errInvalidOrderFilter, arg.OrderFilter)
|
||||
}
|
||||
}
|
||||
switch arg.TriggerPriceType {
|
||||
case "", "LastPrice", "IndexPrice", "MarkPrice":
|
||||
default:
|
||||
return nil, errInvalidTriggerPriceType
|
||||
}
|
||||
var resp OrderResponse
|
||||
|
||||
epl := createOrderEPL
|
||||
if arg.Category == "spot" {
|
||||
if arg.Category == cSpot {
|
||||
epl = createSpotOrderEPL
|
||||
}
|
||||
return &resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodPost, "/v5/order/create", nil, arg, &resp, epl)
|
||||
var resp *OrderResponse
|
||||
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodPost, "/v5/order/create", nil, arg, &resp, epl)
|
||||
}
|
||||
|
||||
// AmendOrder amends an open unfilled or partially filled orders.
|
||||
func (e *Exchange) AmendOrder(ctx context.Context, arg *AmendOrderParams) (*OrderResponse, error) {
|
||||
if arg == nil {
|
||||
return nil, errNilArgument
|
||||
}
|
||||
if arg.OrderID == "" && arg.OrderLinkID == "" {
|
||||
return nil, errEitherOrderIDOROrderLinkIDRequired
|
||||
}
|
||||
err := isValidCategory(arg.Category)
|
||||
if err != nil {
|
||||
func (e *Exchange) AmendOrder(ctx context.Context, arg *AmendOrderRequest) (*OrderResponse, error) {
|
||||
if err := arg.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if arg.Symbol.IsEmpty() {
|
||||
return nil, currency.ErrCurrencyPairEmpty
|
||||
}
|
||||
var resp *OrderResponse
|
||||
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodPost, "/v5/order/amend", nil, arg, &resp, amendOrderEPL)
|
||||
}
|
||||
|
||||
// CancelTradeOrder cancels an open unfilled or partially filled order.
|
||||
func (e *Exchange) CancelTradeOrder(ctx context.Context, arg *CancelOrderParams) (*OrderResponse, error) {
|
||||
if arg == nil {
|
||||
return nil, errNilArgument
|
||||
}
|
||||
if arg.OrderID == "" && arg.OrderLinkID == "" {
|
||||
return nil, errEitherOrderIDOROrderLinkIDRequired
|
||||
}
|
||||
err := isValidCategory(arg.Category)
|
||||
if err != nil {
|
||||
func (e *Exchange) CancelTradeOrder(ctx context.Context, arg *CancelOrderRequest) (*OrderResponse, error) {
|
||||
if err := arg.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if arg.Symbol.IsEmpty() {
|
||||
return nil, currency.ErrCurrencyPairEmpty
|
||||
}
|
||||
switch {
|
||||
case arg.OrderFilter != "" && arg.Category == cSpot:
|
||||
switch arg.OrderFilter {
|
||||
case "Order", "tpslOrder", "StopOrder":
|
||||
default:
|
||||
return nil, fmt.Errorf("%w, orderFilter=%s", errInvalidOrderFilter, arg.OrderFilter)
|
||||
}
|
||||
case arg.OrderFilter != "":
|
||||
return nil, fmt.Errorf("%w, orderFilter is valid for 'spot' only", errInvalidCategory)
|
||||
}
|
||||
var resp *OrderResponse
|
||||
|
||||
epl := cancelOrderEPL
|
||||
if arg.Category == "spot" {
|
||||
if arg.Category == cSpot {
|
||||
epl = cancelSpotEPL
|
||||
}
|
||||
var resp *OrderResponse
|
||||
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodPost, "/v5/order/cancel", nil, arg, &resp, epl)
|
||||
}
|
||||
|
||||
@@ -615,12 +560,12 @@ func (e *Exchange) CancelAllTradeOrders(ctx context.Context, arg *CancelAllOrder
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if arg.OrderFilter != "" && (arg.Category != "linear" && arg.Category != "inverse") {
|
||||
if arg.OrderFilter != "" && (arg.Category != cLinear && arg.Category != cInverse) {
|
||||
return nil, fmt.Errorf("%w, only used for category=linear or inverse", errInvalidOrderFilter)
|
||||
}
|
||||
var resp CancelAllResponse
|
||||
epl := cancelAllEPL
|
||||
if arg.Category == "spot" {
|
||||
if arg.Category == cSpot {
|
||||
epl = cancelAllSpotEPL
|
||||
}
|
||||
return resp.List, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodPost, "/v5/order/cancel-all", nil, arg, &resp, epl)
|
||||
@@ -2490,15 +2435,6 @@ func (e *Exchange) GetBrokerEarning(ctx context.Context, businessType, cursor st
|
||||
return resp.List, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodGet, "/v5/broker/earning-record", params, nil, &resp, defaultEPL)
|
||||
}
|
||||
|
||||
func processOB(ob [][2]types.Number) []orderbook.Level {
|
||||
o := make([]orderbook.Level, len(ob))
|
||||
for x := range ob {
|
||||
o[x].Price = ob[x][0].Float64()
|
||||
o[x].Amount = ob[x][1].Float64()
|
||||
}
|
||||
return o
|
||||
}
|
||||
|
||||
// SendHTTPRequest sends an unauthenticated request
|
||||
func (e *Exchange) SendHTTPRequest(ctx context.Context, ePath exchange.URL, path string, f request.EndpointLimit, result any) error {
|
||||
endpointPath, err := e.API.Endpoints.GetURL(ePath)
|
||||
@@ -2676,6 +2612,20 @@ func (e *Exchange) FetchAccountType(ctx context.Context) (AccountType, error) {
|
||||
return e.account.accountType, nil
|
||||
}
|
||||
|
||||
// String returns the account type as a string
|
||||
func (a AccountType) String() string {
|
||||
switch a {
|
||||
case 0:
|
||||
return "unset"
|
||||
case accountTypeNormal:
|
||||
return "normal"
|
||||
case accountTypeUnified:
|
||||
return "unified"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// RequiresUnifiedAccount checks account type and returns error if not unified
|
||||
func (e *Exchange) RequiresUnifiedAccount(ctx context.Context) error {
|
||||
at, err := e.FetchAccountType(ctx)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -12,8 +12,6 @@ import (
|
||||
"github.com/thrasher-corp/gocryptotrader/types"
|
||||
)
|
||||
|
||||
var validCategory = []string{"spot", "linear", "inverse", "option"}
|
||||
|
||||
// supportedOptionsTypes Bybit does not offer a way to retrieve option denominations via its API
|
||||
var supportedOptionsTypes = []string{"BTC", "ETH", "SOL"}
|
||||
|
||||
@@ -134,11 +132,6 @@ type KlineItem struct {
|
||||
Turnover types.Number
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements the json.Unmarshaler interface for KlineItem
|
||||
func (k *KlineItem) UnmarshalJSON(data []byte) error {
|
||||
return json.Unmarshal(data, &[7]any{&k.StartTime, &k.Open, &k.High, &k.Low, &k.Close, &k.TradeVolume, &k.Turnover})
|
||||
}
|
||||
|
||||
// MarkPriceKlineResponse represents a kline data item.
|
||||
type MarkPriceKlineResponse struct {
|
||||
Symbol string `json:"symbol"`
|
||||
@@ -146,17 +139,6 @@ type MarkPriceKlineResponse struct {
|
||||
List []KlineItem `json:"list"`
|
||||
}
|
||||
|
||||
func constructOrderbook(o *orderbookResponse) (*Orderbook, error) {
|
||||
s := Orderbook{
|
||||
Symbol: o.Symbol,
|
||||
UpdateID: o.UpdateID,
|
||||
GenerationTime: o.Timestamp.Time(),
|
||||
}
|
||||
s.Bids = processOB(o.Bids)
|
||||
s.Asks = processOB(o.Asks)
|
||||
return &s, nil
|
||||
}
|
||||
|
||||
// TickerData represents a list of ticker detailed information.
|
||||
type TickerData struct {
|
||||
Category string `json:"category"`
|
||||
@@ -306,17 +288,17 @@ type DeliveryPrice struct {
|
||||
} `json:"list"`
|
||||
}
|
||||
|
||||
// PlaceOrderParams represents
|
||||
type PlaceOrderParams struct {
|
||||
// PlaceOrderRequest represents
|
||||
type PlaceOrderRequest struct {
|
||||
Category string `json:"category"` // Required
|
||||
Symbol currency.Pair `json:"symbol"` // Required
|
||||
Side string `json:"side"` // Required
|
||||
OrderType string `json:"orderType"` // Required // Market, Limit
|
||||
OrderQuantity float64 `json:"qty,string"` // Required // Order quantity. For Spot Market Buy order, please note that qty should be quote currency amount
|
||||
Price float64 `json:"price,string,omitempty"`
|
||||
TimeInForce string `json:"timeInForce,omitempty"` // IOC and GTC
|
||||
OrderLinkID string `json:"orderLinkId,omitempty"` // User customised order ID. A max of 36 characters. Combinations of numbers, letters (upper and lower cases), dashes, and underscores are supported. future orderLinkId rules:
|
||||
WhetherToBorrow bool `json:"-"` // '0' for default spot, '1' for Margin trading.
|
||||
TimeInForce string `json:"timeInForce,omitempty"` // IOC and GTC
|
||||
OrderLinkID string `json:"orderLinkId,omitempty"` // User customised order ID. A max of 36 characters. Combinations of numbers, letters (upper and lower cases), dashes, and underscores are supported. future orderLinkId rules:
|
||||
EnableBorrow bool `json:"-"`
|
||||
IsLeverage int64 `json:"isLeverage,omitempty"` // Required // '0' for default spot, '1' for Margin trading.
|
||||
OrderFilter string `json:"orderFilter,omitempty"` // Valid for spot only. Order,tpslOrder. If not passed, Order by default
|
||||
TriggerDirection int64 `json:"triggerDirection,omitempty"` // Required // Conditional order param. Used to identify the expected direction of the conditional order. '1': triggered when market price rises to triggerPrice '2': triggered when market price falls to triggerPrice
|
||||
@@ -349,16 +331,18 @@ type OrderResponse struct {
|
||||
OrderLinkID string `json:"orderLinkId"`
|
||||
}
|
||||
|
||||
// AmendOrderParams represents a parameter for amending order.
|
||||
type AmendOrderParams struct {
|
||||
Category string `json:"category,omitempty"`
|
||||
Symbol currency.Pair `json:"symbol,omitzero"`
|
||||
OrderID string `json:"orderId,omitempty"`
|
||||
OrderLinkID string `json:"orderLinkId,omitempty"` // User customised order ID. A max of 36 characters. Combinations of numbers, letters (upper and lower cases), dashes, and underscores are supported. future orderLinkId rules:
|
||||
OrderImpliedVolatility string `json:"orderIv,omitempty"`
|
||||
TriggerPrice float64 `json:"triggerPrice,omitempty,string"`
|
||||
OrderQuantity float64 `json:"qty,omitempty,string"` // Order quantity. For Spot Market Buy order, please note that qty should be quote currency amount
|
||||
Price float64 `json:"price,string,omitempty"`
|
||||
// AmendOrderRequest represents a parameter for amending order.
|
||||
type AmendOrderRequest struct {
|
||||
Category string `json:"category"` // Required
|
||||
Symbol currency.Pair `json:"symbol"` // Required
|
||||
OrderID string `json:"orderId,omitempty"` // This or OrderLinkID required
|
||||
OrderLinkID string `json:"orderLinkId,omitempty"` // User customised order ID. A max of 36 characters. Combinations of numbers, letters (upper and lower cases), dashes, and underscores are supported. future orderLinkId rules:
|
||||
|
||||
// At least one of the following fields is required
|
||||
OrderImpliedVolatility string `json:"orderIv,omitempty"`
|
||||
TriggerPrice float64 `json:"triggerPrice,omitempty,string"`
|
||||
OrderQuantity float64 `json:"qty,omitempty,string"` // Order quantity. For Spot Market Buy order, please note that qty should be quote currency amount
|
||||
Price float64 `json:"price,string,omitempty"`
|
||||
|
||||
TakeProfitPrice float64 `json:"takeProfit,omitempty,string"`
|
||||
StopLossPrice float64 `json:"stopLoss,omitempty,string"`
|
||||
@@ -378,14 +362,13 @@ type AmendOrderParams struct {
|
||||
TPSLMode string `json:"tpslMode,omitempty"`
|
||||
}
|
||||
|
||||
// CancelOrderParams represents a cancel order parameters.
|
||||
type CancelOrderParams struct {
|
||||
Category string `json:"category,omitempty"`
|
||||
Symbol currency.Pair `json:"symbol,omitzero"`
|
||||
// CancelOrderRequest represents a cancel order parameters.
|
||||
type CancelOrderRequest struct {
|
||||
Category string `json:"category"`
|
||||
Symbol currency.Pair `json:"symbol"`
|
||||
OrderID string `json:"orderId,omitempty"`
|
||||
OrderLinkID string `json:"orderLinkId,omitempty"` // User customised order ID. A max of 36 characters. Combinations of numbers, letters (upper and lower cases), dashes, and underscores are supported. future orderLinkId rules:
|
||||
|
||||
OrderFilter string `json:"orderFilter,omitempty"` // Valid for spot only. Order,tpslOrder. If not passed, Order by default
|
||||
OrderFilter string `json:"orderFilter,omitempty"` // Valid for spot only. Order,tpslOrder. If not passed, Order by default
|
||||
}
|
||||
|
||||
// TradeOrders represents category and list of trade orders of the category.
|
||||
@@ -548,8 +531,8 @@ type BatchAmendOrderParamItem struct {
|
||||
|
||||
// CancelBatchOrder represents a batch cancel request parameters.
|
||||
type CancelBatchOrder struct {
|
||||
Category string `json:"category"`
|
||||
Request []CancelOrderParams `json:"request"`
|
||||
Category string `json:"category"`
|
||||
Request []CancelOrderRequest `json:"request"`
|
||||
}
|
||||
|
||||
// CancelBatchResponseItem represents a batch cancel response item.
|
||||
@@ -1775,11 +1758,11 @@ type WsOrderbookDetail struct {
|
||||
|
||||
// SubscriptionResponse represents a subscription response.
|
||||
type SubscriptionResponse struct {
|
||||
Success bool `json:"success"`
|
||||
RetMsg string `json:"ret_msg"`
|
||||
ConnID string `json:"conn_id"`
|
||||
RequestID string `json:"req_id"`
|
||||
Operation string `json:"op"`
|
||||
Success bool `json:"success"`
|
||||
ReturnMessage string `json:"ret_msg"`
|
||||
ConnectionID string `json:"conn_id"`
|
||||
RequestID string `json:"req_id"`
|
||||
Operation string `json:"op"`
|
||||
}
|
||||
|
||||
// WebsocketResponse represents push data response struct.
|
||||
@@ -2055,17 +2038,3 @@ type accountTypeHolder struct {
|
||||
|
||||
// AccountType constants
|
||||
type AccountType uint8
|
||||
|
||||
// String returns the account type as a string
|
||||
func (a AccountType) String() string {
|
||||
switch a {
|
||||
case 0:
|
||||
return "unset"
|
||||
case accountTypeNormal:
|
||||
return "normal"
|
||||
case accountTypeUnified:
|
||||
return "unified"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/buger/jsonparser"
|
||||
gws "github.com/gorilla/websocket"
|
||||
"github.com/thrasher-corp/gocryptotrader/common"
|
||||
"github.com/thrasher-corp/gocryptotrader/common/crypto"
|
||||
@@ -58,6 +59,7 @@ const (
|
||||
|
||||
// Main-net private
|
||||
websocketPrivate = "wss://stream.bybit.com/v5/private"
|
||||
websocketTrade = "wss://stream.bybit.com/v5/trade"
|
||||
)
|
||||
|
||||
var defaultSubscriptions = subscription.List{
|
||||
@@ -97,24 +99,14 @@ func (e *Exchange) WsConnect(ctx context.Context, conn websocket.Connection) err
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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)
|
||||
// WebsocketAuthenticatePrivateConnection sends an authentication message to the private websocket for inbound account
|
||||
// data
|
||||
func (e *Exchange) WebsocketAuthenticatePrivateConnection(ctx context.Context, conn websocket.Connection) error {
|
||||
req, err := e.GetAuthenticationPayload(ctx, strconv.FormatInt(conn.GenerateMessageID(false), 10))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req := Authenticate{
|
||||
RequestID: strconv.FormatInt(conn.GenerateMessageID(false), 10),
|
||||
Operation: "auth",
|
||||
Args: []any{creds.Key, intNonce, hex.EncodeToString(hmac)},
|
||||
}
|
||||
resp, err := conn.SendMessageReturnResponse(ctx, request.Unset, req.RequestID, req)
|
||||
resp, err := conn.SendMessageReturnResponse(ctx, wsSubscriptionEPL, req.RequestID, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -123,11 +115,61 @@ func (e *Exchange) WebsocketAuthenticateConnection(ctx context.Context, conn web
|
||||
return err
|
||||
}
|
||||
if !response.Success {
|
||||
return fmt.Errorf("%s with request ID %s msg: %s", response.Operation, response.RequestID, response.RetMsg)
|
||||
return fmt.Errorf("%s with request ID %s msg: %s", response.Operation, response.RequestID, response.ReturnMessage)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WebsocketAuthenticateTradeConnection sends an authentication message to the private trade websocket for outbound
|
||||
// account data
|
||||
func (e *Exchange) WebsocketAuthenticateTradeConnection(ctx context.Context, conn websocket.Connection) error {
|
||||
// request ID is not returned with the response, a workaround in the trade connection handler monitors the response
|
||||
// for the operation type "auth", which is then set in the response match key.
|
||||
req, err := e.GetAuthenticationPayload(ctx, "auth")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp, err := conn.SendMessageReturnResponse(ctx, wsSubscriptionEPL, req.RequestID, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var response struct {
|
||||
ReturnCode int64 `json:"retCode"`
|
||||
ReturnMessage string `json:"retMsg"`
|
||||
Operation string `json:"op"`
|
||||
ConnectionID string `json:"connId"`
|
||||
}
|
||||
if err := json.Unmarshal(resp, &response); err != nil {
|
||||
return err
|
||||
}
|
||||
if response.ReturnCode != 0 {
|
||||
c, ok := retCode[response.ReturnCode]
|
||||
if !ok {
|
||||
c = "unknown return error code"
|
||||
}
|
||||
return fmt.Errorf("%s failed - code:%d [%v] msg:%s", response.Operation, response.ReturnCode, c, response.ReturnMessage)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAuthenticationPayload returns the authentication payload for the websocket connection to upgrade the connection.
|
||||
func (e *Exchange) GetAuthenticationPayload(ctx context.Context, requestID string) (*Authenticate, error) {
|
||||
creds, err := e.GetCredentials(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
expires := time.Now().Add(time.Hour * 6).UnixMilli()
|
||||
hmac, err := crypto.GetHMAC(crypto.HashSHA256, []byte("GET/realtime"+strconv.FormatInt(expires, 10)), []byte(creds.Secret))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Authenticate{
|
||||
RequestID: requestID,
|
||||
Operation: "auth",
|
||||
Args: []any{creds.Key, expires, hex.EncodeToString(hmac)},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (e *Exchange) handleSubscriptions(conn websocket.Connection, operation string, subs subscription.List) (args []SubscriptionArgument, err error) {
|
||||
subs, err = subs.ExpandTemplates(e)
|
||||
if err != nil {
|
||||
@@ -163,6 +205,29 @@ func (e *Exchange) GetSubscriptionTemplate(_ *subscription.Subscription) (*templ
|
||||
}).Parse(subTplText)
|
||||
}
|
||||
|
||||
func (e *Exchange) wsHandleTradeData(conn websocket.Connection, respRaw []byte) error {
|
||||
var response struct {
|
||||
RequestID string `json:"reqId"`
|
||||
Operation string `json:"op"`
|
||||
}
|
||||
if err := json.Unmarshal(respRaw, &response); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if response.RequestID != "" {
|
||||
return conn.RequireMatchWithData(response.RequestID, respRaw)
|
||||
}
|
||||
|
||||
switch response.Operation {
|
||||
case "auth": // When authenticating the connection there is no request ID, so a static value is used.
|
||||
return conn.RequireMatchWithData(response.Operation, respRaw)
|
||||
case "pong":
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("%w for trade: %v", errUnhandledStreamData, string(respRaw))
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Exchange) wsHandleData(conn websocket.Connection, assetType asset.Item, respRaw []byte) error {
|
||||
var result WebsocketResponse
|
||||
if err := json.Unmarshal(respRaw, &result); err != nil {
|
||||
@@ -208,6 +273,12 @@ func (e *Exchange) wsHandleAuthenticatedData(ctx context.Context, conn websocket
|
||||
case chanExecution:
|
||||
return e.wsProcessExecution(&result)
|
||||
case chanOrder:
|
||||
// Use first order's orderLinkId to match with an entire batch of order change requests
|
||||
if id, err := jsonparser.GetString(respRaw, "data", "[0]", "orderLinkId"); err == nil {
|
||||
if conn.IncomingWithData(id, respRaw) {
|
||||
return nil // If the data has been routed, return
|
||||
}
|
||||
}
|
||||
return e.wsProcessOrder(&result)
|
||||
case chanWallet:
|
||||
return e.wsProcessWalletPushData(ctx, respRaw)
|
||||
@@ -268,7 +339,7 @@ func (e *Exchange) wsProcessWalletPushData(ctx context.Context, resp []byte) err
|
||||
|
||||
// wsProcessOrder the order stream to see changes to your orders in real-time.
|
||||
func (e *Exchange) wsProcessOrder(resp *WebsocketResponse) error {
|
||||
var result WsOrders
|
||||
var result []WebsocketOrderDetails
|
||||
if err := json.Unmarshal(resp.Data, &result); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -282,30 +353,26 @@ func (e *Exchange) wsProcessOrder(resp *WebsocketResponse) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
side, err := order.StringToOrderSide(result[x].Side)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tif, err := order.StringToTimeInForce(result[x].TimeInForce)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
execution[x] = order.Detail{
|
||||
TimeInForce: tif,
|
||||
Amount: result[x].Qty.Float64(),
|
||||
Amount: result[x].Quantity.Float64(),
|
||||
Exchange: e.Name,
|
||||
OrderID: result[x].OrderID,
|
||||
ClientOrderID: result[x].OrderLinkID,
|
||||
Side: side,
|
||||
Side: result[x].Side,
|
||||
Type: orderType,
|
||||
Pair: cp,
|
||||
Cost: result[x].CumExecQty.Float64() * result[x].AvgPrice.Float64(),
|
||||
Fee: result[x].CumExecFee.Float64(),
|
||||
Cost: result[x].CumulativeExecutedQuantity.Float64() * result[x].AveragePrice.Float64(),
|
||||
Fee: result[x].CumulativeExecutedFee.Float64(),
|
||||
AssetType: a,
|
||||
Status: StringToOrderStatus(result[x].OrderStatus),
|
||||
Price: result[x].Price.Float64(),
|
||||
ExecutedAmount: result[x].CumExecQty.Float64(),
|
||||
AverageExecutedPrice: result[x].AvgPrice.Float64(),
|
||||
ExecutedAmount: result[x].CumulativeExecutedQuantity.Float64(),
|
||||
AverageExecutedPrice: result[x].AveragePrice.Float64(),
|
||||
Date: result[x].CreatedTime.Time(),
|
||||
LastUpdated: result[x].UpdatedTime.Time(),
|
||||
}
|
||||
@@ -581,9 +648,6 @@ func (e *Exchange) wsProcessOrderbook(assetType asset.Item, resp *WebsocketRespo
|
||||
if err := json.Unmarshal(resp.Data, &result); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(result.Bids) == 0 && len(result.Asks) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
cp, err := e.MatchSymbolWithAvailablePairs(result.Symbol, assetType, hasPotentialDelimiter(assetType))
|
||||
if err != nil {
|
||||
@@ -620,6 +684,7 @@ func (e *Exchange) wsProcessOrderbook(assetType asset.Item, resp *WebsocketRespo
|
||||
UpdateID: result.UpdateID,
|
||||
UpdateTime: resp.OrderbookLastUpdated.Time(),
|
||||
LastPushed: resp.PushTimestamp.Time(),
|
||||
AllowEmpty: true,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -678,11 +743,11 @@ func (e *Exchange) submitDirectSubscription(ctx context.Context, conn websocket.
|
||||
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 {
|
||||
if err := conn.SendJSONMessage(ctx, wsSubscriptionEPL, payload); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
response, err := conn.SendMessageReturnResponse(ctx, request.Unset, payload.RequestID, payload)
|
||||
response, err := conn.SendMessageReturnResponse(ctx, wsSubscriptionEPL, payload.RequestID, payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -691,7 +756,7 @@ func (e *Exchange) submitDirectSubscription(ctx context.Context, conn websocket.
|
||||
return err
|
||||
}
|
||||
if !resp.Success {
|
||||
return fmt.Errorf("%s with request ID %s msg: %s", resp.Operation, resp.RequestID, resp.RetMsg)
|
||||
return fmt.Errorf("%s with request ID %s msg: %s", resp.Operation, resp.RequestID, resp.ReturnMessage)
|
||||
}
|
||||
}
|
||||
if err := op(conn, payload.associatedSubs...); err != nil {
|
||||
@@ -807,13 +872,13 @@ func (e *Exchange) authUnsubscribe(ctx context.Context, conn websocket.Connectio
|
||||
func (e *Exchange) matchPairAssetFromResponse(category, symbol string) (currency.Pair, asset.Item, error) {
|
||||
assets := make([]asset.Item, 0, 2)
|
||||
switch category {
|
||||
case "spot":
|
||||
case cSpot:
|
||||
assets = append(assets, asset.Spot)
|
||||
case "inverse":
|
||||
case cInverse:
|
||||
assets = append(assets, asset.CoinMarginedFutures)
|
||||
case "linear":
|
||||
case cLinear:
|
||||
assets = append(assets, asset.USDTMarginedFutures, asset.USDCMarginedFutures)
|
||||
case "option":
|
||||
case cOption:
|
||||
assets = append(assets, asset.Options)
|
||||
default:
|
||||
return currency.EMPTYPAIR, 0, fmt.Errorf("incoming symbol %q %w: %q", symbol, errUnsupportedCategory, category)
|
||||
|
||||
141
exchanges/bybit/bybit_websocket_requests.go
Normal file
141
exchanges/bybit/bybit_websocket_requests.go
Normal file
@@ -0,0 +1,141 @@
|
||||
package bybit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/thrasher-corp/gocryptotrader/encoding/json"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
|
||||
)
|
||||
|
||||
// Websocket request operation types
|
||||
const (
|
||||
OutboundTradeConnection = "PRIVATE_TRADE"
|
||||
InboundPrivateConnection = "PRIVATE"
|
||||
)
|
||||
|
||||
// WSCreateOrder creates an order through the websocket connection
|
||||
func (e *Exchange) WSCreateOrder(ctx context.Context, r *PlaceOrderRequest) (*WebsocketOrderDetails, error) {
|
||||
if err := r.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
epl, err := getWSRateLimitEPLByCategory(r.Category)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if r.OrderLinkID == "" {
|
||||
r.OrderLinkID = uuid.Must(uuid.NewV7()).String()
|
||||
}
|
||||
return e.sendWebsocketTradeRequest(ctx, "order.create", r.OrderLinkID, r, epl)
|
||||
}
|
||||
|
||||
// WSAmendOrder amends an order through the websocket connection
|
||||
func (e *Exchange) WSAmendOrder(ctx context.Context, r *AmendOrderRequest) (*WebsocketOrderDetails, error) {
|
||||
if err := r.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
epl, err := getWSRateLimitEPLByCategory(r.Category)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if r.OrderLinkID == "" {
|
||||
r.OrderLinkID = uuid.Must(uuid.NewV7()).String()
|
||||
}
|
||||
return e.sendWebsocketTradeRequest(ctx, "order.amend", r.OrderLinkID, r, epl)
|
||||
}
|
||||
|
||||
// WSCancelOrder cancels an order through the websocket connection
|
||||
func (e *Exchange) WSCancelOrder(ctx context.Context, r *CancelOrderRequest) (*WebsocketOrderDetails, error) {
|
||||
if err := r.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
epl, err := getWSRateLimitEPLByCategory(r.Category)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if r.OrderLinkID == "" {
|
||||
r.OrderLinkID = uuid.Must(uuid.NewV7()).String()
|
||||
}
|
||||
return e.sendWebsocketTradeRequest(ctx, "order.cancel", r.OrderLinkID, r, epl)
|
||||
}
|
||||
|
||||
// sendWebsocketTradeRequest sends a trade request to the exchange through the websocket connection
|
||||
func (e *Exchange) sendWebsocketTradeRequest(ctx context.Context, op, orderLinkID string, payload any, limit request.EndpointLimit) (*WebsocketOrderDetails, error) {
|
||||
// Get the outbound and inbound connections to send and receive the request. This makes sure both are live before
|
||||
// sending the request.
|
||||
outbound, err := e.Websocket.GetConnection(OutboundTradeConnection)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
inbound, err := e.Websocket.GetConnection(InboundPrivateConnection)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tn := time.Now()
|
||||
requestID := strconv.FormatInt(outbound.GenerateMessageID(false), 10)
|
||||
|
||||
// Set up a listener to wait for the response to come back from the inbound connection. The request is sent through
|
||||
// the outbound trade connection, the response can come back through the inbound private connection before the
|
||||
// outbound connection sends its acknowledgement.
|
||||
ch, err := inbound.MatchReturnResponses(ctx, orderLinkID, 1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
outResp, err := outbound.SendMessageReturnResponse(ctx, limit, requestID, WebsocketGeneralPayload{
|
||||
RequestID: requestID,
|
||||
Header: map[string]string{"X-BAPI-TIMESTAMP": strconv.FormatInt(tn.UnixMilli(), 10)},
|
||||
Operation: op,
|
||||
Arguments: []any{payload},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var confirmation WebsocketConfirmation
|
||||
if err := json.Unmarshal(outResp, &confirmation); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if confirmation.RetCode != 0 {
|
||||
return nil, fmt.Errorf("code:%d, info:%v message:%s", confirmation.RetCode, retCode[confirmation.RetCode], confirmation.RetMsg)
|
||||
}
|
||||
|
||||
inResp := <-ch // Blocking read is acceptable; channel has a built in timeout already
|
||||
if inResp.Err != nil {
|
||||
return nil, inResp.Err
|
||||
}
|
||||
|
||||
if len(inResp.Responses) != 1 {
|
||||
return nil, fmt.Errorf("expected 1 response, received %d", len(inResp.Responses))
|
||||
}
|
||||
|
||||
var ret WebsocketOrderResponse
|
||||
if err := json.Unmarshal(inResp.Responses[0], &ret); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(ret.Data) != 1 {
|
||||
return nil, fmt.Errorf("expected 1 response, received %d", len(ret.Data))
|
||||
}
|
||||
|
||||
if ret.Data[0].RejectReason != "EC_NoError" {
|
||||
return nil, fmt.Errorf("order rejected: %s", ret.Data[0].RejectReason)
|
||||
}
|
||||
|
||||
return &ret.Data[0], nil
|
||||
}
|
||||
|
||||
var retCode = map[int64]string{
|
||||
10404: "either op type is not found or category is not correct/supported",
|
||||
10429: "request exceeds rate limit",
|
||||
20006: "reqId is duplicated",
|
||||
10016: "internal server error",
|
||||
10019: "ws trade service is restarting, please reconnect",
|
||||
20003: "too frequent requests under the same session",
|
||||
10403: "exceed IP rate limit. 3000 requests per second per IP",
|
||||
}
|
||||
210
exchanges/bybit/bybit_websocket_requests_test.go
Normal file
210
exchanges/bybit/bybit_websocket_requests_test.go
Normal file
@@ -0,0 +1,210 @@
|
||||
package bybit
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/thrasher-corp/gocryptotrader/config"
|
||||
"github.com/thrasher-corp/gocryptotrader/currency"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchange/order/limits"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/sharedtestvalues"
|
||||
testutils "github.com/thrasher-corp/gocryptotrader/internal/testing/utils"
|
||||
)
|
||||
|
||||
func TestWSCreateOrder(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
arg := &PlaceOrderRequest{}
|
||||
_, err := e.WSCreateOrder(t.Context(), arg)
|
||||
require.ErrorIs(t, err, errCategoryNotSet)
|
||||
|
||||
arg.Category = cSpot
|
||||
_, err = e.WSCreateOrder(t.Context(), arg)
|
||||
require.ErrorIs(t, err, currency.ErrCurrencyPairEmpty)
|
||||
|
||||
arg.Symbol = currency.NewBTCUSDT()
|
||||
_, err = e.WSCreateOrder(t.Context(), arg)
|
||||
require.ErrorIs(t, err, order.ErrSideIsInvalid)
|
||||
|
||||
arg.Side = "Buy"
|
||||
_, err = e.WSCreateOrder(t.Context(), arg)
|
||||
require.ErrorIs(t, err, order.ErrTypeIsInvalid)
|
||||
|
||||
arg.OrderType = "Limit"
|
||||
_, err = e.WSCreateOrder(t.Context(), arg)
|
||||
require.ErrorIs(t, err, limits.ErrAmountBelowMin)
|
||||
|
||||
arg.OrderQuantity = 0.0001
|
||||
arg.TriggerDirection = 69
|
||||
_, err = e.WSCreateOrder(t.Context(), arg)
|
||||
require.ErrorIs(t, err, errInvalidTriggerDirection)
|
||||
|
||||
arg.TriggerDirection = 0
|
||||
arg.OrderFilter = "dodgy"
|
||||
_, err = e.WSCreateOrder(t.Context(), arg)
|
||||
require.ErrorIs(t, err, errInvalidOrderFilter)
|
||||
|
||||
arg.OrderFilter = "Order"
|
||||
arg.TriggerPriceType = "dodgy"
|
||||
_, err = e.WSCreateOrder(t.Context(), arg)
|
||||
require.ErrorIs(t, err, errInvalidTriggerPriceType)
|
||||
|
||||
sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders)
|
||||
e := getWebsocketInstance(t) //nolint:govet // Intentional shadow
|
||||
got, err := e.WSCreateOrder(t.Context(), &PlaceOrderRequest{
|
||||
Category: cSpot,
|
||||
Symbol: currency.NewBTCUSDT(),
|
||||
Side: "Buy",
|
||||
OrderType: "Limit",
|
||||
Price: 55000,
|
||||
OrderQuantity: -0.0001, // Replace with a valid quantity
|
||||
TimeInForce: "FOK", // Replace with GTC to submit a valid order if outside current trading price range.
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, got)
|
||||
}
|
||||
|
||||
func TestWebsocketSubmitOrder(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Test quote amount needs to be used due to protocol trade requirements
|
||||
s := &order.Submit{
|
||||
Exchange: e.Name,
|
||||
Pair: currency.NewBTCUSDT(),
|
||||
AssetType: asset.Spot,
|
||||
Side: order.Buy,
|
||||
Type: order.Market,
|
||||
Amount: 0.0001,
|
||||
}
|
||||
|
||||
_, err := e.WebsocketSubmitOrder(t.Context(), s)
|
||||
require.ErrorIs(t, err, order.ErrAmountMustBeSet)
|
||||
|
||||
sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders)
|
||||
e := getWebsocketInstance(t) //nolint:govet // Intentional shadow
|
||||
|
||||
s.Type = order.Limit
|
||||
s.Price = 55000
|
||||
s.Amount = -0.0001 // Replace with a valid quantity
|
||||
got, err := e.WebsocketSubmitOrder(t.Context(), s)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, got)
|
||||
}
|
||||
|
||||
func TestWSAmendOrder(t *testing.T) {
|
||||
t.Parallel()
|
||||
arg := &AmendOrderRequest{}
|
||||
_, err := e.WSAmendOrder(t.Context(), arg)
|
||||
require.ErrorIs(t, err, errCategoryNotSet)
|
||||
|
||||
arg.Category = cSpot
|
||||
_, err = e.WSAmendOrder(t.Context(), arg)
|
||||
require.ErrorIs(t, err, currency.ErrCurrencyPairEmpty)
|
||||
|
||||
arg.Symbol = currency.NewBTCUSDT()
|
||||
_, err = e.WSAmendOrder(t.Context(), arg)
|
||||
require.ErrorIs(t, err, errEitherOrderIDOROrderLinkIDRequired)
|
||||
|
||||
sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders)
|
||||
e := getWebsocketInstance(t) //nolint:govet // Intentional shadow
|
||||
arg.OrderID = "1793353687809485568" // Replace with a valid order ID
|
||||
arg.OrderQuantity = 0.0002
|
||||
got, err := e.WSAmendOrder(t.Context(), arg)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, got)
|
||||
}
|
||||
|
||||
func TestWebsocketModifyOrder(t *testing.T) {
|
||||
t.Parallel()
|
||||
mod := &order.Modify{
|
||||
Pair: currency.NewBTCUSDT(),
|
||||
AssetType: asset.Spot,
|
||||
Amount: 0.0001,
|
||||
OrderID: "1793388409122024192", // Replace with a valid order ID
|
||||
}
|
||||
|
||||
sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders)
|
||||
e := getWebsocketInstance(t) //nolint:govet // Intentional shadow
|
||||
|
||||
got, err := e.WebsocketModifyOrder(t.Context(), mod)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, got)
|
||||
}
|
||||
|
||||
func TestWSCancelOrder(t *testing.T) {
|
||||
t.Parallel()
|
||||
arg := &CancelOrderRequest{}
|
||||
_, err := e.WSCancelOrder(t.Context(), arg)
|
||||
require.ErrorIs(t, err, errCategoryNotSet)
|
||||
|
||||
arg.Category = cSpot
|
||||
_, err = e.WSCancelOrder(t.Context(), arg)
|
||||
require.ErrorIs(t, err, currency.ErrCurrencyPairEmpty)
|
||||
|
||||
arg.Symbol = currency.NewBTCUSDT()
|
||||
_, err = e.WSCancelOrder(t.Context(), arg)
|
||||
require.ErrorIs(t, err, errEitherOrderIDOROrderLinkIDRequired)
|
||||
|
||||
arg.OrderID = "1793353687809485568" // Replace with a valid order ID
|
||||
|
||||
arg.OrderFilter = "dodgy"
|
||||
_, err = e.WSCancelOrder(t.Context(), arg)
|
||||
require.ErrorIs(t, err, errInvalidOrderFilter)
|
||||
|
||||
arg.Category = cLinear
|
||||
_, err = e.WSCancelOrder(t.Context(), arg)
|
||||
require.ErrorIs(t, err, errInvalidCategory)
|
||||
|
||||
arg.Category = cSpot
|
||||
arg.OrderFilter = "Order"
|
||||
|
||||
sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders)
|
||||
e := getWebsocketInstance(t) //nolint:govet // Intentional shadow
|
||||
got, err := e.WSCancelOrder(t.Context(), arg)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, got)
|
||||
}
|
||||
|
||||
func TestWebsocketCancelOrder(t *testing.T) {
|
||||
t.Parallel()
|
||||
cancel := &order.Cancel{
|
||||
OrderID: "1793388409122024192", // Replace with a valid order ID
|
||||
Pair: currency.NewBTCUSDT(),
|
||||
AssetType: asset.Spot,
|
||||
}
|
||||
|
||||
sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders)
|
||||
e := getWebsocketInstance(t) //nolint:govet // Intentional shadow
|
||||
|
||||
err := e.WebsocketCancelOrder(t.Context(), cancel)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// getWebsocketInstance returns a websocket instance copy for live bi-directional testing
|
||||
func getWebsocketInstance(t *testing.T) *Exchange {
|
||||
t.Helper()
|
||||
cfg := &config.Config{}
|
||||
root, err := testutils.RootPathFromCWD()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = cfg.LoadConfig(filepath.Join(root, "testdata", "configtest.json"), true)
|
||||
require.NoError(t, err)
|
||||
|
||||
pairs := &e.CurrencyPairs
|
||||
e := new(Exchange) //nolint:govet // Intentional shadow
|
||||
e.SetDefaults()
|
||||
bConf, err := cfg.GetExchangeConfig("Bybit")
|
||||
require.NoError(t, err)
|
||||
bConf.API.AuthenticatedSupport = true
|
||||
bConf.API.AuthenticatedWebsocketSupport = true
|
||||
bConf.API.Credentials.Key = apiKey
|
||||
bConf.API.Credentials.Secret = apiSecret
|
||||
|
||||
require.NoError(t, e.Setup(bConf), "Setup must not error")
|
||||
e.CurrencyPairs.Load(pairs)
|
||||
require.NoError(t, e.Websocket.Connect())
|
||||
return e
|
||||
}
|
||||
85
exchanges/bybit/bybit_websocket_requests_types.go
Normal file
85
exchanges/bybit/bybit_websocket_requests_types.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package bybit
|
||||
|
||||
import (
|
||||
"github.com/thrasher-corp/gocryptotrader/currency"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
|
||||
"github.com/thrasher-corp/gocryptotrader/types"
|
||||
)
|
||||
|
||||
// WebsocketOrderDetails is the order details from the websocket response.
|
||||
type WebsocketOrderDetails struct {
|
||||
Category string `json:"category"`
|
||||
OrderID string `json:"orderId"`
|
||||
OrderLinkID string `json:"orderLinkId"`
|
||||
IsLeverage string `json:"isLeverage"` // Whether to borrow. Unified spot only. 0: false, 1: true; Classic spot is not supported, always 0
|
||||
BlockTradeID string `json:"blockTradeId"`
|
||||
Symbol string `json:"symbol"` // Undelimited so inbuilt string used.
|
||||
Price types.Number `json:"price"`
|
||||
Quantity types.Number `json:"qty"`
|
||||
Side order.Side `json:"side"`
|
||||
PositionIdx int64 `json:"positionIdx"`
|
||||
OrderStatus string `json:"orderStatus"`
|
||||
CreateType string `json:"createType"`
|
||||
CancelType string `json:"cancelType"`
|
||||
RejectReason string `json:"rejectReason"` // Classic spot is not supported
|
||||
AveragePrice types.Number `json:"avgPrice"`
|
||||
LeavesQuantity types.Number `json:"leavesQty"` // The remaining qty not executed. Classic spot is not supported
|
||||
LeavesValue types.Number `json:"leavesValue"` // The remaining value not executed. Classic spot is not supported
|
||||
CumulativeExecutedQuantity types.Number `json:"cumExecQty"`
|
||||
CumulativeExecutedValue types.Number `json:"cumExecValue"`
|
||||
CumulativeExecutedFee types.Number `json:"cumExecFee"`
|
||||
ClosedPNL types.Number `json:"closedPnl"`
|
||||
FeeCurrency currency.Code `json:"feeCurrency"` // Trading fee currency for Spot only.
|
||||
TimeInForce string `json:"timeInForce"`
|
||||
OrderType string `json:"orderType"`
|
||||
StopOrderType string `json:"stopOrderType"`
|
||||
OneCancelsOtherTriggerBy string `json:"ocoTriggerBy"` // UTA Spot: add new response field ocoTriggerBy, and the value can be OcoTriggerByUnknown, OcoTriggerByTp, OcoTriggerBySl
|
||||
OrderImpliedVolatility types.Number `json:"orderIv"`
|
||||
MarketUnit string `json:"marketUnit"` // The unit for qty when create Spot market orders for UTA account. baseCoin, quoteCoin
|
||||
TriggerPrice types.Number `json:"triggerPrice"` // Trigger price. If stopOrderType=TrailingStop, it is activate price. Otherwise, it is trigger price
|
||||
TakeProfit types.Number `json:"takeProfit"`
|
||||
StopLoss types.Number `json:"stopLoss"`
|
||||
TakeProfitStopLossMode string `json:"tpslMode"` // TP/SL mode, Full: entire position for TP/SL. Partial: partial position tp/sl. Spot does not have this field, and Option returns always ""
|
||||
TakeProfitLimitPrice types.Number `json:"tpLimitPrice"`
|
||||
StopLossLimitPrice types.Number `json:"slLimitPrice"`
|
||||
TakeProfitTriggerBy string `json:"tpTriggerBy"`
|
||||
StopLossTriggerBy string `json:"slTriggerBy"`
|
||||
TriggerDirection int64 `json:"triggerDirection"` // Trigger direction. 1: rise, 2: fall
|
||||
TriggerBy string `json:"triggerBy"`
|
||||
LastPriceOnCreated types.Number `json:"lastPriceOnCreated"`
|
||||
ReduceOnly bool `json:"reduceOnly"`
|
||||
CloseOnTrigger bool `json:"closeOnTrigger"`
|
||||
PlaceType string `json:"placeType"` // Place type, option used. iv, price
|
||||
SMPType string `json:"smpType"`
|
||||
SMPGroup int `json:"smpGroup"`
|
||||
SMPOrderID string `json:"smpOrderId"`
|
||||
CreatedTime types.Time `json:"createdTime"`
|
||||
UpdatedTime types.Time `json:"updatedTime"`
|
||||
}
|
||||
|
||||
// WebsocketConfirmation is the initial response from the websocket connection
|
||||
type WebsocketConfirmation struct {
|
||||
RequestID string `json:"reqId"`
|
||||
RetCode int64 `json:"retCode"`
|
||||
RetMsg string `json:"retMsg"`
|
||||
Operation string `json:"op"`
|
||||
RequestAcknowledgement OrderResponse `json:"data"`
|
||||
Header map[string]string `json:"header"`
|
||||
ConnectionID string `json:"connId"`
|
||||
}
|
||||
|
||||
// WebsocketOrderResponse is the response from an order request through the websocket connection
|
||||
type WebsocketOrderResponse struct {
|
||||
ID string `json:"id"`
|
||||
Topic string `json:"topic"`
|
||||
CreationTime types.Time `json:"creationTime"`
|
||||
Data []WebsocketOrderDetails `json:"data"`
|
||||
}
|
||||
|
||||
// WebsocketGeneralPayload is the general payload for websocket requests
|
||||
type WebsocketGeneralPayload struct {
|
||||
RequestID string `json:"reqId"`
|
||||
Header map[string]string `json:"header"`
|
||||
Operation string `json:"op"`
|
||||
Arguments []any `json:"args"`
|
||||
}
|
||||
40
exchanges/bybit/bybit_websocket_test.go
Normal file
40
exchanges/bybit/bybit_websocket_test.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package bybit
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchange/websocket"
|
||||
)
|
||||
|
||||
func TestWSHandleTradeData(t *testing.T) {
|
||||
t.Parallel()
|
||||
for _, tc := range []struct {
|
||||
input []byte
|
||||
match string
|
||||
err error
|
||||
}{
|
||||
{input: []byte(`{"reqId":"12345"}`), match: "12345", err: nil},
|
||||
{input: []byte(`{"op":"auth"}`), match: "auth", err: nil},
|
||||
{input: []byte(`{"op":"pong"}`), err: nil},
|
||||
{input: []byte(`{"op":"pewpewpew"}`), err: errUnhandledStreamData},
|
||||
} {
|
||||
conn := &FixtureConnection{match: websocket.NewMatch()}
|
||||
var ch <-chan []byte
|
||||
if tc.match != "" {
|
||||
var err error
|
||||
ch, err = conn.match.Set(tc.match, 1)
|
||||
require.NoError(t, err, "match.Set must not error")
|
||||
}
|
||||
err := e.wsHandleTradeData(conn, tc.input)
|
||||
if tc.err != nil {
|
||||
require.ErrorIs(t, err, tc.err)
|
||||
continue
|
||||
}
|
||||
require.NoError(t, err)
|
||||
if tc.match != "" {
|
||||
require.Len(t, ch, 1, "must receive 1 message from channel")
|
||||
require.Equal(t, tc.input, <-ch, "must be correct")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -96,6 +96,9 @@ func (e *Exchange) SetDefaults() {
|
||||
currency.NewCode("SHIB1000"): currency.SHIB,
|
||||
},
|
||||
),
|
||||
TradingRequirements: protocol.TradingRequirements{
|
||||
SpotMarketBuyQuotation: true,
|
||||
},
|
||||
Supports: exchange.FeaturesSupported{
|
||||
REST: true,
|
||||
Websocket: true,
|
||||
@@ -194,16 +197,14 @@ func (e *Exchange) SetDefaults() {
|
||||
exchange.WebsocketUSDTMargined: linearPublic,
|
||||
exchange.WebsocketUSDCMargined: linearPublic,
|
||||
exchange.WebsocketOptions: optionPublic,
|
||||
exchange.WebsocketTrade: websocketTrade,
|
||||
exchange.WebsocketPrivate: websocketPrivate,
|
||||
})
|
||||
if err != nil {
|
||||
log.Errorln(log.ExchangeSys, err)
|
||||
}
|
||||
|
||||
if e.Requester, err = request.New(e.Name,
|
||||
common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout),
|
||||
request.WithLimiter(GetRateLimit()),
|
||||
); err != nil {
|
||||
if e.Requester, err = request.New(e.Name, common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout), request.WithLimiter(rateLimits)); err != nil {
|
||||
log.Errorln(log.ExchangeSys, err)
|
||||
}
|
||||
|
||||
@@ -232,6 +233,7 @@ func (e *Exchange) Setup(exch *config.Exchange) error {
|
||||
OrderbookBufferConfig: buffer.Config{SortBuffer: true, SortBufferByUpdateIDs: true},
|
||||
TradeFeed: e.Features.Enabled.TradeFeed,
|
||||
UseMultiConnectionManagement: true,
|
||||
RateLimitDefinitions: rateLimits,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -241,12 +243,11 @@ func (e *Exchange) Setup(exch *config.Exchange) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Spot
|
||||
// Spot - Inbound public data.
|
||||
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,
|
||||
@@ -264,12 +265,11 @@ func (e *Exchange) Setup(exch *config.Exchange) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Options
|
||||
// Options - Inbound public data.
|
||||
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,
|
||||
@@ -287,12 +287,11 @@ func (e *Exchange) Setup(exch *config.Exchange) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Linear - USDT margined futures.
|
||||
// Linear - USDT margined futures inbound public data.
|
||||
if err := e.Websocket.SetupNewConnection(&websocket.ConnectionSetup{
|
||||
URL: wsUSDTLinearURL,
|
||||
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
|
||||
ResponseMaxLimit: exch.WebsocketResponseMaxLimit,
|
||||
RateLimit: request.NewWeightedRateLimitByDuration(time.Microsecond),
|
||||
Connector: e.WsConnect,
|
||||
GenerateSubscriptions: func() (subscription.List, error) {
|
||||
return e.GenerateLinearDefaultSubscriptions(asset.USDTMarginedFutures)
|
||||
@@ -317,12 +316,11 @@ func (e *Exchange) Setup(exch *config.Exchange) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Linear - USDC margined futures.
|
||||
// Linear - USDC margined futures inbound public data.
|
||||
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)
|
||||
@@ -347,12 +345,11 @@ func (e *Exchange) Setup(exch *config.Exchange) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Inverse - Coin margined futures.
|
||||
// Inverse - Coin margined futures inbound public data.
|
||||
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,
|
||||
@@ -365,17 +362,38 @@ func (e *Exchange) Setup(exch *config.Exchange) error {
|
||||
return err
|
||||
}
|
||||
|
||||
wsTradeURL, err := e.API.Endpoints.GetURL(exchange.WebsocketTrade)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Trade - Dedicated trade connection for all outbound trading requests.
|
||||
if err := e.Websocket.SetupNewConnection(&websocket.ConnectionSetup{
|
||||
URL: wsTradeURL,
|
||||
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
|
||||
ResponseMaxLimit: exch.WebsocketResponseMaxLimit,
|
||||
Connector: e.WsConnect,
|
||||
Handler: func(_ context.Context, conn websocket.Connection, resp []byte) error {
|
||||
return e.wsHandleTradeData(conn, resp)
|
||||
},
|
||||
RequestIDGenerator: e.messageIDSeq.IncrementAndGet,
|
||||
Authenticate: e.WebsocketAuthenticateTradeConnection,
|
||||
MessageFilter: OutboundTradeConnection,
|
||||
SubscriptionsNotRequired: true,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
wsPrivateURL, err := e.API.Endpoints.GetURL(exchange.WebsocketPrivate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Private
|
||||
// Private - Inbound private data connection for authenticated data
|
||||
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,
|
||||
@@ -383,7 +401,8 @@ func (e *Exchange) Setup(exch *config.Exchange) error {
|
||||
Unsubscriber: e.authUnsubscribe,
|
||||
Handler: e.wsHandleAuthenticatedData,
|
||||
RequestIDGenerator: e.messageIDSeq.IncrementAndGet,
|
||||
Authenticate: e.WebsocketAuthenticateConnection,
|
||||
Authenticate: e.WebsocketAuthenticatePrivateConnection,
|
||||
MessageFilter: InboundPrivateConnection,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -467,13 +486,13 @@ func (e *Exchange) FetchTradablePairs(ctx context.Context, a asset.Item) (curren
|
||||
func getCategoryName(a asset.Item) string {
|
||||
switch a {
|
||||
case asset.CoinMarginedFutures:
|
||||
return "inverse"
|
||||
return cInverse
|
||||
case asset.USDTMarginedFutures, asset.USDCMarginedFutures:
|
||||
return "linear"
|
||||
return cLinear
|
||||
case asset.Spot:
|
||||
return a.String()
|
||||
case asset.Options:
|
||||
return "option"
|
||||
return cOption
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
@@ -604,10 +623,7 @@ func (e *Exchange) UpdateOrderbook(ctx context.Context, p currency.Pair, assetTy
|
||||
}
|
||||
|
||||
switch assetType {
|
||||
case asset.Spot, asset.USDTMarginedFutures,
|
||||
asset.USDCMarginedFutures,
|
||||
asset.CoinMarginedFutures,
|
||||
asset.Options:
|
||||
case asset.Spot, asset.USDTMarginedFutures, asset.USDCMarginedFutures, asset.CoinMarginedFutures, asset.Options:
|
||||
if assetType == asset.USDCMarginedFutures && !p.Quote.Equal(currency.PERP) {
|
||||
p.Delimiter = currency.DashDelimiter
|
||||
}
|
||||
@@ -866,79 +882,52 @@ func orderTypeToString(oType order.Type) string {
|
||||
|
||||
// SubmitOrder submits a new order
|
||||
func (e *Exchange) SubmitOrder(ctx context.Context, s *order.Submit) (*order.SubmitResponse, error) {
|
||||
err := s.Validate(e.GetTradingRequirements())
|
||||
arg, err := e.deriveSubmitOrderArguments(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
formattedPair, err := e.FormatExchangeCurrency(s.Pair, s.AssetType)
|
||||
response, err := e.PlaceOrder(ctx, arg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var sideType string
|
||||
switch {
|
||||
case s.Side.IsLong():
|
||||
sideType = sideBuy
|
||||
case s.Side.IsShort():
|
||||
sideType = sideSell
|
||||
default:
|
||||
return nil, order.ErrSideIsInvalid
|
||||
resp, err := s.DeriveSubmitResponse(response.OrderID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
status := order.New
|
||||
switch s.AssetType {
|
||||
case asset.Spot, asset.Options, asset.USDTMarginedFutures, asset.USDCMarginedFutures, asset.CoinMarginedFutures:
|
||||
if s.AssetType == asset.USDCMarginedFutures && !formattedPair.Quote.Equal(currency.PERP) {
|
||||
formattedPair.Delimiter = currency.DashDelimiter
|
||||
}
|
||||
var response *OrderResponse
|
||||
arg := &PlaceOrderParams{
|
||||
Category: getCategoryName(s.AssetType),
|
||||
Symbol: formattedPair,
|
||||
Side: sideType,
|
||||
OrderType: orderTypeToString(s.Type),
|
||||
OrderQuantity: s.Amount,
|
||||
Price: s.Price,
|
||||
OrderLinkID: s.ClientOrderID,
|
||||
WhetherToBorrow: s.AssetType == asset.Margin,
|
||||
ReduceOnly: s.ReduceOnly,
|
||||
OrderFilter: func() string {
|
||||
if s.RiskManagementModes.TakeProfit.Price != 0 || s.RiskManagementModes.TakeProfit.LimitPrice != 0 ||
|
||||
s.RiskManagementModes.StopLoss.Price != 0 || s.RiskManagementModes.StopLoss.LimitPrice != 0 {
|
||||
return ""
|
||||
} else if s.TriggerPrice != 0 {
|
||||
return "tpslOrder"
|
||||
}
|
||||
return "Order"
|
||||
}(),
|
||||
TriggerPrice: s.TriggerPrice,
|
||||
}
|
||||
if arg.TriggerPrice != 0 {
|
||||
arg.TriggerPriceType = s.TriggerPriceType.String()
|
||||
}
|
||||
if s.RiskManagementModes.TakeProfit.Price != 0 {
|
||||
arg.TakeProfitPrice = s.RiskManagementModes.TakeProfit.Price
|
||||
arg.TakeProfitTriggerBy = s.RiskManagementModes.TakeProfit.TriggerPriceType.String()
|
||||
arg.TpOrderType = getOrderTypeString(s.RiskManagementModes.TakeProfit.OrderType)
|
||||
arg.TpLimitPrice = s.RiskManagementModes.TakeProfit.LimitPrice
|
||||
}
|
||||
if s.RiskManagementModes.StopLoss.Price != 0 {
|
||||
arg.StopLossPrice = s.RiskManagementModes.StopLoss.Price
|
||||
arg.StopLossTriggerBy = s.RiskManagementModes.StopLoss.TriggerPriceType.String()
|
||||
arg.SlOrderType = getOrderTypeString(s.RiskManagementModes.StopLoss.OrderType)
|
||||
arg.SlLimitPrice = s.RiskManagementModes.StopLoss.LimitPrice
|
||||
}
|
||||
response, err = e.PlaceOrder(ctx, arg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := s.DeriveSubmitResponse(response.OrderID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp.Status = status
|
||||
return resp, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("%s %w", s.AssetType, asset.ErrNotSupported)
|
||||
resp.Status = order.New
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// WebsocketSubmitOrder submits a new order through the websocket connection
|
||||
func (e *Exchange) WebsocketSubmitOrder(ctx context.Context, s *order.Submit) (*order.SubmitResponse, error) {
|
||||
arg, err := e.deriveSubmitOrderArguments(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
orderDetails, err := e.WSCreateOrder(ctx, arg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := s.DeriveSubmitResponse(orderDetails.OrderID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp.Status, err = order.StringToOrderStatus(orderDetails.OrderStatus)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp.TimeInForce, err = order.StringToTimeInForce(orderDetails.TimeInForce)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp.ReduceOnly = orderDetails.ReduceOnly
|
||||
resp.TriggerPrice = orderDetails.TriggerPrice.Float64()
|
||||
resp.AverageExecutedPrice = orderDetails.AveragePrice.Float64()
|
||||
resp.ClientOrderID = orderDetails.OrderLinkID
|
||||
resp.Fee = orderDetails.CumulativeExecutedFee.Float64()
|
||||
resp.Cost = orderDetails.CumulativeExecutedValue.Float64()
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func getOrderTypeString(oType order.Type) string {
|
||||
@@ -950,48 +939,13 @@ func getOrderTypeString(oType order.Type) string {
|
||||
}
|
||||
}
|
||||
|
||||
// ModifyOrder will allow of changing orderbook placement and limit to
|
||||
// market conversion
|
||||
// ModifyOrder will allow of changing orderbook placement and limit to market conversion
|
||||
func (e *Exchange) ModifyOrder(ctx context.Context, action *order.Modify) (*order.ModifyResponse, error) {
|
||||
if err := action.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var (
|
||||
result *OrderResponse
|
||||
err error
|
||||
)
|
||||
action.Pair, err = e.FormatExchangeCurrency(action.Pair, action.AssetType)
|
||||
arg, err := e.deriveAmendOrderArguments(action)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch action.AssetType {
|
||||
case asset.Spot, asset.USDTMarginedFutures, asset.USDCMarginedFutures, asset.CoinMarginedFutures, asset.Options:
|
||||
if action.AssetType == asset.USDCMarginedFutures && !action.Pair.Quote.Equal(currency.PERP) {
|
||||
action.Pair.Delimiter = currency.DashDelimiter
|
||||
}
|
||||
arg := &AmendOrderParams{
|
||||
Category: getCategoryName(action.AssetType),
|
||||
Symbol: action.Pair,
|
||||
OrderID: action.OrderID,
|
||||
OrderLinkID: action.ClientOrderID,
|
||||
OrderQuantity: action.Amount,
|
||||
Price: action.Price,
|
||||
TriggerPrice: action.TriggerPrice,
|
||||
TriggerPriceType: action.TriggerPriceType.String(),
|
||||
TakeProfitPrice: action.RiskManagementModes.TakeProfit.Price,
|
||||
TakeProfitTriggerBy: getOrderTypeString(action.RiskManagementModes.TakeProfit.OrderType),
|
||||
TakeProfitLimitPrice: action.RiskManagementModes.TakeProfit.LimitPrice,
|
||||
StopLossPrice: action.RiskManagementModes.StopLoss.Price,
|
||||
StopLossTriggerBy: action.RiskManagementModes.StopLoss.TriggerPriceType.String(),
|
||||
StopLossLimitPrice: action.RiskManagementModes.StopLoss.LimitPrice,
|
||||
}
|
||||
result, err = e.AmendOrder(ctx, arg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
default:
|
||||
err = fmt.Errorf("%s %w", action.AssetType, asset.ErrNotSupported)
|
||||
}
|
||||
result, err := e.AmendOrder(ctx, arg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1003,29 +957,44 @@ func (e *Exchange) ModifyOrder(ctx context.Context, action *order.Modify) (*orde
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// WebsocketModifyOrder modifies an existing order
|
||||
func (e *Exchange) WebsocketModifyOrder(ctx context.Context, action *order.Modify) (*order.ModifyResponse, error) {
|
||||
arg, err := e.deriveAmendOrderArguments(action)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result, err := e.WSAmendOrder(ctx, arg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := action.DeriveModifyResponse()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp.OrderID = result.OrderID
|
||||
resp.ClientOrderID = result.OrderLinkID
|
||||
resp.Amount = result.Quantity.Float64()
|
||||
resp.Price = action.Price
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// CancelOrder cancels an order by its corresponding ID number
|
||||
func (e *Exchange) CancelOrder(ctx context.Context, ord *order.Cancel) error {
|
||||
if err := ord.Validate(ord.StandardCancel()); err != nil {
|
||||
return err
|
||||
}
|
||||
format, err := e.GetPairFormat(ord.AssetType, true)
|
||||
arg, err := e.deriveCancelOrderArguments(ord)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch ord.AssetType {
|
||||
case asset.Spot, asset.USDTMarginedFutures, asset.USDCMarginedFutures, asset.CoinMarginedFutures, asset.Options:
|
||||
if ord.AssetType == asset.USDCMarginedFutures && !ord.Pair.Quote.Equal(currency.PERP) {
|
||||
ord.Pair.Delimiter = currency.DashDelimiter
|
||||
}
|
||||
_, err = e.CancelTradeOrder(ctx, &CancelOrderParams{
|
||||
Category: getCategoryName(ord.AssetType),
|
||||
Symbol: ord.Pair.Format(format),
|
||||
OrderID: ord.OrderID,
|
||||
OrderLinkID: ord.ClientOrderID,
|
||||
})
|
||||
default:
|
||||
return fmt.Errorf("%s %w", ord.AssetType, asset.ErrNotSupported)
|
||||
_, err = e.CancelTradeOrder(ctx, arg)
|
||||
return err
|
||||
}
|
||||
|
||||
// WebsocketCancelOrder cancels an order by ID
|
||||
func (e *Exchange) WebsocketCancelOrder(ctx context.Context, ord *order.Cancel) error {
|
||||
arg, err := e.deriveCancelOrderArguments(ord)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = e.WSCancelOrder(ctx, arg)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1034,7 +1003,7 @@ func (e *Exchange) CancelBatchOrders(ctx context.Context, o []order.Cancel) (*or
|
||||
if len(o) == 0 {
|
||||
return nil, order.ErrCancelOrderIsNil
|
||||
}
|
||||
requests := make([]CancelOrderParams, len(o))
|
||||
requests := make([]CancelOrderRequest, len(o))
|
||||
category := asset.Options
|
||||
var err error
|
||||
for i := range o {
|
||||
@@ -1053,7 +1022,7 @@ func (e *Exchange) CancelBatchOrders(ctx context.Context, o []order.Cancel) (*or
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
requests[i] = CancelOrderParams{
|
||||
requests[i] = CancelOrderRequest{
|
||||
OrderID: o[i].OrderID,
|
||||
OrderLinkID: o[i].ClientOrderID,
|
||||
Symbol: o[i].Pair,
|
||||
@@ -1828,7 +1797,7 @@ func (e *Exchange) GetFuturesContractDetails(ctx context.Context, item asset.Ite
|
||||
}
|
||||
return resp, nil
|
||||
case asset.USDCMarginedFutures:
|
||||
linearContracts, err := e.GetInstrumentInfo(ctx, "linear", "", "", "", "", 1000)
|
||||
linearContracts, err := e.GetInstrumentInfo(ctx, cLinear, "", "", "", "", 1000)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1907,7 +1876,7 @@ func (e *Exchange) GetFuturesContractDetails(ctx context.Context, item asset.Ite
|
||||
}
|
||||
return resp, nil
|
||||
case asset.USDTMarginedFutures:
|
||||
linearContracts, err := e.GetInstrumentInfo(ctx, "linear", "", "", "", "", 1000)
|
||||
linearContracts, err := e.GetInstrumentInfo(ctx, cLinear, "", "", "", "", 1000)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ func TestGenerateLinearDefaultSubscriptions(t *testing.T) {
|
||||
_, err := e.GenerateLinearDefaultSubscriptions(asset.OptionCombo)
|
||||
assert.ErrorIs(t, err, asset.ErrInvalidAsset)
|
||||
|
||||
e := new(Exchange)
|
||||
e := new(Exchange) //nolint:govet // Intentional shadow
|
||||
require.NoError(t, testexch.Setup(e), "Test instance Setup must not error")
|
||||
|
||||
subs, err := e.GenerateLinearDefaultSubscriptions(asset.USDTMarginedFutures)
|
||||
@@ -43,7 +43,7 @@ func TestGenerateLinearDefaultSubscriptions(t *testing.T) {
|
||||
func TestLinearSubscribe(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")
|
||||
|
||||
subs, err := e.GenerateLinearDefaultSubscriptions(asset.USDTMarginedFutures)
|
||||
@@ -60,7 +60,7 @@ func TestLinearSubscribe(t *testing.T) {
|
||||
func TestLinearUnsubscribe(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")
|
||||
|
||||
subs, err := e.GenerateLinearDefaultSubscriptions(asset.USDTMarginedFutures)
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
|
||||
func TestGenerateOptionsDefaultSubscriptions(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")
|
||||
subs, err := e.GenerateOptionsDefaultSubscriptions()
|
||||
require.NoError(t, err, "GenerateOptionsDefaultSubscriptions must not error")
|
||||
@@ -31,7 +31,7 @@ func TestGenerateOptionsDefaultSubscriptions(t *testing.T) {
|
||||
func TestOptionSubscribe(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")
|
||||
|
||||
subs, err := e.GenerateOptionsDefaultSubscriptions()
|
||||
@@ -44,7 +44,7 @@ func TestOptionSubscribe(t *testing.T) {
|
||||
func TestOptionsUnsubscribe(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")
|
||||
|
||||
subs, err := e.GenerateOptionsDefaultSubscriptions()
|
||||
|
||||
132
exchanges/bybit/order_arguments.go
Normal file
132
exchanges/bybit/order_arguments.go
Normal file
@@ -0,0 +1,132 @@
|
||||
package bybit
|
||||
|
||||
import (
|
||||
"github.com/thrasher-corp/gocryptotrader/currency"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
|
||||
)
|
||||
|
||||
func (e *Exchange) deriveSubmitOrderArguments(s *order.Submit) (*PlaceOrderRequest, error) {
|
||||
if err := s.Validate(e.GetTradingRequirements()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
formattedPair, err := e.FormatExchangeCurrency(s.Pair, s.AssetType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
side := sideBuy
|
||||
if s.Side.IsShort() {
|
||||
side = sideSell
|
||||
}
|
||||
|
||||
if s.AssetType == asset.USDCMarginedFutures && !formattedPair.Quote.Equal(currency.PERP) {
|
||||
formattedPair.Delimiter = currency.DashDelimiter
|
||||
}
|
||||
|
||||
timeInForce := "GTC"
|
||||
if s.Type == order.Market {
|
||||
timeInForce = "IOC"
|
||||
} else {
|
||||
switch {
|
||||
case s.TimeInForce.Is(order.FillOrKill):
|
||||
timeInForce = "FOK"
|
||||
case s.TimeInForce.Is(order.PostOnly):
|
||||
timeInForce = "PostOnly"
|
||||
case s.TimeInForce.Is(order.ImmediateOrCancel):
|
||||
timeInForce = "IOC"
|
||||
}
|
||||
}
|
||||
|
||||
orderFilter := "" // If "Order" is not passed, "Order" by default.
|
||||
if s.AssetType == asset.Spot && s.TriggerPrice != 0 {
|
||||
orderFilter = "tpslOrder"
|
||||
}
|
||||
|
||||
var triggerPriceType string
|
||||
if s.TriggerPrice != 0 {
|
||||
triggerPriceType = s.TriggerPriceType.String()
|
||||
}
|
||||
|
||||
arg := &PlaceOrderRequest{
|
||||
Category: getCategoryName(s.AssetType),
|
||||
Symbol: formattedPair,
|
||||
Side: side,
|
||||
OrderType: orderTypeToString(s.Type),
|
||||
OrderQuantity: s.Amount,
|
||||
Price: s.Price,
|
||||
OrderLinkID: s.ClientOrderID,
|
||||
EnableBorrow: s.AssetType == asset.Margin,
|
||||
ReduceOnly: s.ReduceOnly,
|
||||
OrderFilter: orderFilter,
|
||||
TriggerPrice: s.TriggerPrice,
|
||||
TimeInForce: timeInForce,
|
||||
TriggerPriceType: triggerPriceType,
|
||||
}
|
||||
|
||||
if s.RiskManagementModes.TakeProfit.Price != 0 {
|
||||
arg.TakeProfitPrice = s.RiskManagementModes.TakeProfit.Price
|
||||
arg.TakeProfitTriggerBy = s.RiskManagementModes.TakeProfit.TriggerPriceType.String()
|
||||
arg.TpOrderType = getOrderTypeString(s.RiskManagementModes.TakeProfit.OrderType)
|
||||
arg.TpLimitPrice = s.RiskManagementModes.TakeProfit.LimitPrice
|
||||
}
|
||||
if s.RiskManagementModes.StopLoss.Price != 0 {
|
||||
arg.StopLossPrice = s.RiskManagementModes.StopLoss.Price
|
||||
arg.StopLossTriggerBy = s.RiskManagementModes.StopLoss.TriggerPriceType.String()
|
||||
arg.SlOrderType = getOrderTypeString(s.RiskManagementModes.StopLoss.OrderType)
|
||||
arg.SlLimitPrice = s.RiskManagementModes.StopLoss.LimitPrice
|
||||
}
|
||||
return arg, nil
|
||||
}
|
||||
|
||||
func (e *Exchange) deriveAmendOrderArguments(action *order.Modify) (*AmendOrderRequest, error) {
|
||||
if err := action.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pair, err := e.FormatExchangeCurrency(action.Pair, action.AssetType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if action.AssetType == asset.USDCMarginedFutures && !pair.Quote.Equal(currency.PERP) {
|
||||
pair.Delimiter = currency.DashDelimiter
|
||||
}
|
||||
|
||||
return &AmendOrderRequest{
|
||||
Category: getCategoryName(action.AssetType),
|
||||
Symbol: pair,
|
||||
OrderID: action.OrderID,
|
||||
OrderLinkID: action.ClientOrderID,
|
||||
OrderQuantity: action.Amount,
|
||||
Price: action.Price,
|
||||
TriggerPrice: action.TriggerPrice,
|
||||
TriggerPriceType: action.TriggerPriceType.String(),
|
||||
TakeProfitPrice: action.RiskManagementModes.TakeProfit.Price,
|
||||
TakeProfitTriggerBy: getOrderTypeString(action.RiskManagementModes.TakeProfit.OrderType),
|
||||
TakeProfitLimitPrice: action.RiskManagementModes.TakeProfit.LimitPrice,
|
||||
StopLossPrice: action.RiskManagementModes.StopLoss.Price,
|
||||
StopLossTriggerBy: action.RiskManagementModes.StopLoss.TriggerPriceType.String(),
|
||||
StopLossLimitPrice: action.RiskManagementModes.StopLoss.LimitPrice,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (e *Exchange) deriveCancelOrderArguments(ord *order.Cancel) (*CancelOrderRequest, error) {
|
||||
if err := ord.Validate(ord.StandardCancel()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pair, err := e.FormatExchangeCurrency(ord.Pair, ord.AssetType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ord.AssetType == asset.USDCMarginedFutures && !pair.Quote.Equal(currency.PERP) {
|
||||
pair.Delimiter = currency.DashDelimiter
|
||||
}
|
||||
return &CancelOrderRequest{
|
||||
Category: getCategoryName(ord.AssetType),
|
||||
Symbol: pair,
|
||||
OrderID: ord.OrderID,
|
||||
OrderLinkID: ord.ClientOrderID,
|
||||
}, nil
|
||||
}
|
||||
239
exchanges/bybit/order_arguments_test.go
Normal file
239
exchanges/bybit/order_arguments_test.go
Normal file
@@ -0,0 +1,239 @@
|
||||
package bybit
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/thrasher-corp/gocryptotrader/currency"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
|
||||
)
|
||||
|
||||
func TestDeriveSubmitOrderArguments(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, tc := range []struct {
|
||||
submit *order.Submit
|
||||
exp *PlaceOrderRequest
|
||||
err error
|
||||
}{
|
||||
{err: order.ErrSubmissionIsNil},
|
||||
{
|
||||
submit: &order.Submit{
|
||||
Exchange: e.GetName(),
|
||||
Pair: currency.NewBTCUSDT(),
|
||||
AssetType: asset.Binary,
|
||||
Side: order.Buy,
|
||||
Type: order.Market,
|
||||
Amount: 1,
|
||||
},
|
||||
err: asset.ErrNotSupported,
|
||||
},
|
||||
{
|
||||
submit: &order.Submit{
|
||||
Exchange: e.GetName(),
|
||||
Pair: currency.NewBTCUSDT(),
|
||||
AssetType: asset.USDCMarginedFutures,
|
||||
Side: order.Buy,
|
||||
Type: order.Market,
|
||||
Amount: 1,
|
||||
},
|
||||
exp: &PlaceOrderRequest{
|
||||
Category: getCategoryName(asset.USDCMarginedFutures),
|
||||
Symbol: currency.NewBTCUSDT().Format(currency.PairFormat{Uppercase: true, Delimiter: currency.DashDelimiter}),
|
||||
Side: sideBuy,
|
||||
OrderType: orderTypeToString(order.Market),
|
||||
OrderQuantity: 1,
|
||||
TimeInForce: "IOC",
|
||||
},
|
||||
},
|
||||
{
|
||||
submit: &order.Submit{
|
||||
Exchange: e.GetName(),
|
||||
Pair: currency.NewBTCUSDT(),
|
||||
AssetType: asset.USDCMarginedFutures,
|
||||
Side: order.Buy,
|
||||
Type: order.Market,
|
||||
Amount: 1,
|
||||
RiskManagementModes: order.RiskManagementModes{
|
||||
TakeProfit: order.RiskManagement{Price: 100},
|
||||
StopLoss: order.RiskManagement{Price: 200},
|
||||
},
|
||||
},
|
||||
exp: &PlaceOrderRequest{
|
||||
Category: getCategoryName(asset.USDCMarginedFutures),
|
||||
Symbol: currency.NewBTCUSDT().Format(currency.PairFormat{Uppercase: true, Delimiter: currency.DashDelimiter}),
|
||||
Side: sideBuy,
|
||||
OrderType: orderTypeToString(order.Market),
|
||||
OrderQuantity: 1,
|
||||
TimeInForce: "IOC",
|
||||
TakeProfitPrice: 100,
|
||||
TakeProfitTriggerBy: "LastPrice",
|
||||
StopLossPrice: 200,
|
||||
StopLossTriggerBy: "LastPrice",
|
||||
},
|
||||
},
|
||||
{
|
||||
submit: &order.Submit{
|
||||
Exchange: e.GetName(),
|
||||
Pair: currency.NewBTCUSDT(),
|
||||
AssetType: asset.USDCMarginedFutures,
|
||||
Side: order.Sell,
|
||||
Type: order.Limit,
|
||||
Amount: 1,
|
||||
Price: 5000,
|
||||
TriggerPrice: 150,
|
||||
TimeInForce: order.FillOrKill,
|
||||
},
|
||||
exp: &PlaceOrderRequest{
|
||||
Category: getCategoryName(asset.USDCMarginedFutures),
|
||||
Symbol: currency.NewBTCUSDT().Format(currency.PairFormat{Uppercase: true, Delimiter: currency.DashDelimiter}),
|
||||
Side: sideSell,
|
||||
OrderType: orderTypeToString(order.Limit),
|
||||
OrderQuantity: 1,
|
||||
TimeInForce: "FOK",
|
||||
TriggerPrice: 150,
|
||||
Price: 5000,
|
||||
TriggerPriceType: "LastPrice",
|
||||
},
|
||||
},
|
||||
{
|
||||
submit: &order.Submit{
|
||||
Exchange: e.GetName(),
|
||||
Pair: currency.NewBTCUSDT(),
|
||||
AssetType: asset.USDCMarginedFutures,
|
||||
Side: order.Sell,
|
||||
Type: order.Limit,
|
||||
Amount: 1,
|
||||
Price: 5000,
|
||||
TriggerPrice: 150,
|
||||
TimeInForce: order.PostOnly,
|
||||
},
|
||||
exp: &PlaceOrderRequest{
|
||||
Category: getCategoryName(asset.USDCMarginedFutures),
|
||||
Symbol: currency.NewBTCUSDT().Format(currency.PairFormat{Uppercase: true, Delimiter: currency.DashDelimiter}),
|
||||
Side: sideSell,
|
||||
OrderType: orderTypeToString(order.Limit),
|
||||
OrderQuantity: 1,
|
||||
TimeInForce: "PostOnly",
|
||||
TriggerPrice: 150,
|
||||
Price: 5000,
|
||||
TriggerPriceType: "LastPrice",
|
||||
},
|
||||
},
|
||||
{
|
||||
submit: &order.Submit{
|
||||
Exchange: e.GetName(),
|
||||
Pair: currency.NewBTCUSDT(),
|
||||
AssetType: asset.Spot,
|
||||
Side: order.Sell,
|
||||
Type: order.Limit,
|
||||
Amount: 1,
|
||||
Price: 5000,
|
||||
TriggerPrice: 150,
|
||||
TimeInForce: order.ImmediateOrCancel,
|
||||
},
|
||||
exp: &PlaceOrderRequest{
|
||||
Category: getCategoryName(asset.Spot),
|
||||
Symbol: currency.NewBTCUSDT().Format(currency.PairFormat{Uppercase: true}),
|
||||
Side: sideSell,
|
||||
OrderType: orderTypeToString(order.Limit),
|
||||
OrderQuantity: 1,
|
||||
TimeInForce: "IOC",
|
||||
TriggerPrice: 150,
|
||||
Price: 5000,
|
||||
OrderFilter: "tpslOrder",
|
||||
TriggerPriceType: "LastPrice",
|
||||
},
|
||||
},
|
||||
} {
|
||||
got, err := e.deriveSubmitOrderArguments(tc.submit)
|
||||
if tc.err != nil {
|
||||
require.ErrorIs(t, err, tc.err)
|
||||
continue
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.exp, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeriveAmendOrderArguments(t *testing.T) {
|
||||
t.Parallel()
|
||||
for _, tc := range []struct {
|
||||
action *order.Modify
|
||||
exp *AmendOrderRequest
|
||||
err error
|
||||
}{
|
||||
{err: order.ErrModifyOrderIsNil},
|
||||
{
|
||||
action: &order.Modify{
|
||||
OrderID: "69420",
|
||||
Pair: currency.NewBTCUSDT(),
|
||||
AssetType: asset.Binary,
|
||||
},
|
||||
err: asset.ErrNotSupported,
|
||||
},
|
||||
{
|
||||
action: &order.Modify{
|
||||
OrderID: "69420",
|
||||
Pair: currency.NewBTCUSDT(),
|
||||
AssetType: asset.USDCMarginedFutures,
|
||||
},
|
||||
exp: &AmendOrderRequest{
|
||||
Category: getCategoryName(asset.USDCMarginedFutures),
|
||||
Symbol: currency.NewBTCUSDT().Format(currency.PairFormat{Uppercase: true, Delimiter: currency.DashDelimiter}),
|
||||
OrderID: "69420",
|
||||
StopLossTriggerBy: "LastPrice",
|
||||
TriggerPriceType: "LastPrice",
|
||||
},
|
||||
},
|
||||
} {
|
||||
got, err := e.deriveAmendOrderArguments(tc.action)
|
||||
if tc.err != nil {
|
||||
require.ErrorIs(t, err, tc.err)
|
||||
continue
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.exp, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeriveCancelOrderArguments(t *testing.T) {
|
||||
t.Parallel()
|
||||
for _, tc := range []struct {
|
||||
action *order.Cancel
|
||||
exp *CancelOrderRequest
|
||||
err error
|
||||
}{
|
||||
{err: order.ErrCancelOrderIsNil},
|
||||
{
|
||||
action: &order.Cancel{
|
||||
OrderID: "69420",
|
||||
Pair: currency.NewBTCUSDT(),
|
||||
AssetType: asset.Binary,
|
||||
},
|
||||
err: asset.ErrNotSupported,
|
||||
},
|
||||
{
|
||||
action: &order.Cancel{
|
||||
OrderID: "69420",
|
||||
Pair: currency.NewBTCUSDT(),
|
||||
AssetType: asset.USDCMarginedFutures,
|
||||
},
|
||||
exp: &CancelOrderRequest{
|
||||
Category: getCategoryName(asset.USDCMarginedFutures),
|
||||
Symbol: currency.NewBTCUSDT().Format(currency.PairFormat{Uppercase: true, Delimiter: currency.DashDelimiter}),
|
||||
OrderID: "69420",
|
||||
},
|
||||
},
|
||||
} {
|
||||
got, err := e.deriveCancelOrderArguments(tc.action)
|
||||
if tc.err != nil {
|
||||
require.ErrorIs(t, err, tc.err)
|
||||
continue
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.exp, got)
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,15 @@
|
||||
package bybit
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
|
||||
)
|
||||
|
||||
var errUnknownCategory = errors.New("unknown category")
|
||||
|
||||
const (
|
||||
defaultEPL request.EndpointLimit = iota
|
||||
createOrderEPL
|
||||
@@ -66,69 +70,93 @@ const (
|
||||
spotCrossMarginTradeLoanEPL
|
||||
spotCrossMarginTradeRepayEPL
|
||||
spotCrossMarginTradeSwitchEPL
|
||||
|
||||
wsOrderSpotEPL
|
||||
wsOrderInverseEPL
|
||||
wsOrderLinearEPL
|
||||
wsOrderOptionsEPL
|
||||
wsSubscriptionEPL
|
||||
)
|
||||
|
||||
// GetRateLimit returns the rate limit for the exchange
|
||||
func GetRateLimit() request.RateLimitDefinitions {
|
||||
return request.RateLimitDefinitions{
|
||||
defaultEPL: request.NewRateLimitWithWeight(time.Second*5 /* See: https://bybit-exchange.github.io/docs/v5/rate-limit */, 600, 1),
|
||||
createOrderEPL: request.NewRateLimitWithWeight(time.Second, 10, 10),
|
||||
createSpotOrderEPL: request.NewRateLimitWithWeight(time.Second, 20, 20),
|
||||
amendOrderEPL: request.NewRateLimitWithWeight(time.Second, 10, 10),
|
||||
cancelOrderEPL: request.NewRateLimitWithWeight(time.Second, 10, 10),
|
||||
cancelSpotEPL: request.NewRateLimitWithWeight(time.Second, 20, 20),
|
||||
cancelAllEPL: request.NewWeightedRateLimitByDuration(time.Second),
|
||||
cancelAllSpotEPL: request.NewRateLimitWithWeight(time.Second, 20, 20),
|
||||
createBatchOrderEPL: request.NewRateLimitWithWeight(time.Second, 10, 10),
|
||||
amendBatchOrderEPL: request.NewRateLimitWithWeight(time.Second, 10, 10),
|
||||
cancelBatchOrderEPL: request.NewRateLimitWithWeight(time.Second, 10, 10),
|
||||
getOrderEPL: request.NewRateLimitWithWeight(time.Second, 10, 10),
|
||||
getOrderHistoryEPL: request.NewRateLimitWithWeight(time.Second, 10, 10),
|
||||
getPositionListEPL: request.NewRateLimitWithWeight(time.Second, 10, 10),
|
||||
getExecutionListEPL: request.NewRateLimitWithWeight(time.Second, 10, 10),
|
||||
getPositionClosedPNLEPL: request.NewRateLimitWithWeight(time.Second, 10, 10),
|
||||
postPositionSetLeverageEPL: request.NewRateLimitWithWeight(time.Second, 10, 10),
|
||||
setPositionTPLSModeEPL: request.NewRateLimitWithWeight(time.Second, 10, 10),
|
||||
setPositionRiskLimitEPL: request.NewRateLimitWithWeight(time.Second, 10, 10),
|
||||
stopTradingPositionEPL: request.NewRateLimitWithWeight(time.Second, 10, 10),
|
||||
getAccountWalletBalanceEPL: request.NewRateLimitWithWeight(time.Second, 10, 10),
|
||||
getAccountFeeEPL: request.NewRateLimitWithWeight(time.Second, 10, 10),
|
||||
getAssetTransferQueryInfoEPL: request.NewRateLimitWithWeight(time.Minute, 60, 1),
|
||||
getAssetTransferQueryTransferCoinListEPL: request.NewRateLimitWithWeight(time.Minute, 60, 1),
|
||||
getAssetTransferCoinListEPL: request.NewRateLimitWithWeight(time.Minute, 60, 1),
|
||||
getAssetInterTransferListEPL: request.NewRateLimitWithWeight(time.Minute, 60, 1),
|
||||
getSubMemberListEPL: request.NewRateLimitWithWeight(time.Minute, 60, 1),
|
||||
getAssetUniversalTransferListEPL: request.NewRateLimitWithWeight(time.Second, 2, 2),
|
||||
getAssetAccountCoinBalanceEPL: request.NewRateLimitWithWeight(time.Second, 2, 2),
|
||||
getAssetDepositRecordsEPL: request.NewRateLimitWithWeight(time.Minute, 30, 1),
|
||||
getAssetDepositSubMemberRecordsEPL: request.NewRateLimitWithWeight(time.Minute, 30, 1),
|
||||
getAssetDepositSubMemberAddressEPL: request.NewRateLimitWithWeight(time.Minute, 30, 1),
|
||||
getWithdrawRecordsEPL: request.NewRateLimitWithWeight(time.Minute, 30, 1),
|
||||
getAssetCoinInfoEPL: request.NewRateLimitWithWeight(time.Minute, 30, 1),
|
||||
getExchangeOrderRecordEPL: request.NewRateLimitWithWeight(time.Minute, 30, 1),
|
||||
interTransferEPL: request.NewRateLimitWithWeight(time.Minute, 20, 1),
|
||||
saveTransferSubMemberEPL: request.NewRateLimitWithWeight(time.Minute, 20, 1),
|
||||
universalTransferEPL: request.NewRateLimitWithWeight(time.Second, 5, 5),
|
||||
createWithdrawalEPL: request.NewWeightedRateLimitByDuration(time.Second),
|
||||
cancelWithdrawalEPL: request.NewRateLimitWithWeight(time.Minute, 60, 1),
|
||||
userCreateSubMemberEPL: request.NewRateLimitWithWeight(time.Second, 5, 5),
|
||||
userCreateSubAPIKeyEPL: request.NewRateLimitWithWeight(time.Second, 5, 5),
|
||||
userFrozenSubMemberEPL: request.NewRateLimitWithWeight(time.Second, 5, 5),
|
||||
userUpdateAPIEPL: request.NewRateLimitWithWeight(time.Second, 5, 5),
|
||||
userUpdateSubAPIEPL: request.NewRateLimitWithWeight(time.Second, 5, 5),
|
||||
userDeleteAPIEPL: request.NewRateLimitWithWeight(time.Second, 5, 5),
|
||||
userDeleteSubAPIEPL: request.NewRateLimitWithWeight(time.Second, 5, 5),
|
||||
userQuerySubMembersEPL: request.NewRateLimitWithWeight(time.Second, 10, 10),
|
||||
userQueryAPIEPL: request.NewRateLimitWithWeight(time.Second, 10, 10),
|
||||
getSpotLeverageTokenOrderRecordsEPL: request.NewRateLimitWithWeight(time.Second, 50, 50),
|
||||
spotLeverageTokenPurchaseEPL: request.NewRateLimitWithWeight(time.Second, 20, 20),
|
||||
spotLeverTokenRedeemEPL: request.NewRateLimitWithWeight(time.Second, 20, 20),
|
||||
getSpotCrossMarginTradeLoanInfoEPL: request.NewRateLimitWithWeight(time.Second, 50, 50),
|
||||
getSpotCrossMarginTradeAccountEPL: request.NewRateLimitWithWeight(time.Second, 50, 50),
|
||||
getSpotCrossMarginTradeOrdersEPL: request.NewRateLimitWithWeight(time.Second, 50, 50),
|
||||
getSpotCrossMarginTradeRepayHistoryEPL: request.NewRateLimitWithWeight(time.Second, 50, 50),
|
||||
spotCrossMarginTradeLoanEPL: request.NewRateLimitWithWeight(time.Second, 20, 50),
|
||||
spotCrossMarginTradeRepayEPL: request.NewRateLimitWithWeight(time.Second, 20, 50),
|
||||
spotCrossMarginTradeSwitchEPL: request.NewRateLimitWithWeight(time.Second, 20, 50),
|
||||
var rateLimits = request.RateLimitDefinitions{
|
||||
defaultEPL: request.NewRateLimitWithWeight(time.Second*5, 600, 1), // See: https://bybit-exchange.github.io/docs/v5/rate-limit
|
||||
createOrderEPL: request.NewRateLimitWithWeight(time.Second, 10, 10),
|
||||
createSpotOrderEPL: request.NewRateLimitWithWeight(time.Second, 20, 20),
|
||||
amendOrderEPL: request.NewRateLimitWithWeight(time.Second, 10, 10),
|
||||
cancelOrderEPL: request.NewRateLimitWithWeight(time.Second, 10, 10),
|
||||
cancelSpotEPL: request.NewRateLimitWithWeight(time.Second, 20, 20),
|
||||
cancelAllEPL: request.NewWeightedRateLimitByDuration(time.Second),
|
||||
cancelAllSpotEPL: request.NewRateLimitWithWeight(time.Second, 20, 20),
|
||||
createBatchOrderEPL: request.NewRateLimitWithWeight(time.Second, 10, 10),
|
||||
amendBatchOrderEPL: request.NewRateLimitWithWeight(time.Second, 10, 10),
|
||||
cancelBatchOrderEPL: request.NewRateLimitWithWeight(time.Second, 10, 10),
|
||||
getOrderEPL: request.NewRateLimitWithWeight(time.Second, 10, 10),
|
||||
getOrderHistoryEPL: request.NewRateLimitWithWeight(time.Second, 10, 10),
|
||||
getPositionListEPL: request.NewRateLimitWithWeight(time.Second, 10, 10),
|
||||
getExecutionListEPL: request.NewRateLimitWithWeight(time.Second, 10, 10),
|
||||
getPositionClosedPNLEPL: request.NewRateLimitWithWeight(time.Second, 10, 10),
|
||||
postPositionSetLeverageEPL: request.NewRateLimitWithWeight(time.Second, 10, 10),
|
||||
setPositionTPLSModeEPL: request.NewRateLimitWithWeight(time.Second, 10, 10),
|
||||
setPositionRiskLimitEPL: request.NewRateLimitWithWeight(time.Second, 10, 10),
|
||||
stopTradingPositionEPL: request.NewRateLimitWithWeight(time.Second, 10, 10),
|
||||
getAccountWalletBalanceEPL: request.NewRateLimitWithWeight(time.Second, 10, 10),
|
||||
getAccountFeeEPL: request.NewRateLimitWithWeight(time.Second, 10, 10),
|
||||
getAssetTransferQueryInfoEPL: request.NewRateLimitWithWeight(time.Minute, 60, 1),
|
||||
getAssetTransferQueryTransferCoinListEPL: request.NewRateLimitWithWeight(time.Minute, 60, 1),
|
||||
getAssetTransferCoinListEPL: request.NewRateLimitWithWeight(time.Minute, 60, 1),
|
||||
getAssetInterTransferListEPL: request.NewRateLimitWithWeight(time.Minute, 60, 1),
|
||||
getSubMemberListEPL: request.NewRateLimitWithWeight(time.Minute, 60, 1),
|
||||
getAssetUniversalTransferListEPL: request.NewRateLimitWithWeight(time.Second, 2, 2),
|
||||
getAssetAccountCoinBalanceEPL: request.NewRateLimitWithWeight(time.Second, 2, 2),
|
||||
getAssetDepositRecordsEPL: request.NewRateLimitWithWeight(time.Minute, 30, 1),
|
||||
getAssetDepositSubMemberRecordsEPL: request.NewRateLimitWithWeight(time.Minute, 30, 1),
|
||||
getAssetDepositSubMemberAddressEPL: request.NewRateLimitWithWeight(time.Minute, 30, 1),
|
||||
getWithdrawRecordsEPL: request.NewRateLimitWithWeight(time.Minute, 30, 1),
|
||||
getAssetCoinInfoEPL: request.NewRateLimitWithWeight(time.Minute, 30, 1),
|
||||
getExchangeOrderRecordEPL: request.NewRateLimitWithWeight(time.Minute, 30, 1),
|
||||
interTransferEPL: request.NewRateLimitWithWeight(time.Minute, 20, 1),
|
||||
saveTransferSubMemberEPL: request.NewRateLimitWithWeight(time.Minute, 20, 1),
|
||||
universalTransferEPL: request.NewRateLimitWithWeight(time.Second, 5, 5),
|
||||
createWithdrawalEPL: request.NewWeightedRateLimitByDuration(time.Second),
|
||||
cancelWithdrawalEPL: request.NewRateLimitWithWeight(time.Minute, 60, 1),
|
||||
userCreateSubMemberEPL: request.NewRateLimitWithWeight(time.Second, 5, 5),
|
||||
userCreateSubAPIKeyEPL: request.NewRateLimitWithWeight(time.Second, 5, 5),
|
||||
userFrozenSubMemberEPL: request.NewRateLimitWithWeight(time.Second, 5, 5),
|
||||
userUpdateAPIEPL: request.NewRateLimitWithWeight(time.Second, 5, 5),
|
||||
userUpdateSubAPIEPL: request.NewRateLimitWithWeight(time.Second, 5, 5),
|
||||
userDeleteAPIEPL: request.NewRateLimitWithWeight(time.Second, 5, 5),
|
||||
userDeleteSubAPIEPL: request.NewRateLimitWithWeight(time.Second, 5, 5),
|
||||
userQuerySubMembersEPL: request.NewRateLimitWithWeight(time.Second, 10, 10),
|
||||
userQueryAPIEPL: request.NewRateLimitWithWeight(time.Second, 10, 10),
|
||||
getSpotLeverageTokenOrderRecordsEPL: request.NewRateLimitWithWeight(time.Second, 50, 50),
|
||||
spotLeverageTokenPurchaseEPL: request.NewRateLimitWithWeight(time.Second, 20, 20),
|
||||
spotLeverTokenRedeemEPL: request.NewRateLimitWithWeight(time.Second, 20, 20),
|
||||
getSpotCrossMarginTradeLoanInfoEPL: request.NewRateLimitWithWeight(time.Second, 50, 50),
|
||||
getSpotCrossMarginTradeAccountEPL: request.NewRateLimitWithWeight(time.Second, 50, 50),
|
||||
getSpotCrossMarginTradeOrdersEPL: request.NewRateLimitWithWeight(time.Second, 50, 50),
|
||||
getSpotCrossMarginTradeRepayHistoryEPL: request.NewRateLimitWithWeight(time.Second, 50, 50),
|
||||
spotCrossMarginTradeLoanEPL: request.NewRateLimitWithWeight(time.Second, 20, 50),
|
||||
spotCrossMarginTradeRepayEPL: request.NewRateLimitWithWeight(time.Second, 20, 50),
|
||||
spotCrossMarginTradeSwitchEPL: request.NewRateLimitWithWeight(time.Second, 20, 50),
|
||||
|
||||
wsOrderSpotEPL: request.NewRateLimitWithWeight(time.Second, 20, 1),
|
||||
wsOrderInverseEPL: request.NewRateLimitWithWeight(time.Second, 20, 1),
|
||||
wsOrderLinearEPL: request.NewRateLimitWithWeight(time.Second, 20, 1),
|
||||
wsOrderOptionsEPL: request.NewRateLimitWithWeight(time.Second, 20, 1),
|
||||
wsSubscriptionEPL: request.RateLimitNotRequired,
|
||||
}
|
||||
|
||||
func getWSRateLimitEPLByCategory(category string) (request.EndpointLimit, error) {
|
||||
switch category {
|
||||
case cSpot:
|
||||
return wsOrderSpotEPL, nil
|
||||
case cInverse:
|
||||
return wsOrderInverseEPL, nil
|
||||
case cLinear:
|
||||
return wsOrderLinearEPL, nil
|
||||
case cOption:
|
||||
return wsOrderOptionsEPL, nil
|
||||
default:
|
||||
return 0, fmt.Errorf("%w: %q", errUnknownCategory, category)
|
||||
}
|
||||
}
|
||||
|
||||
35
exchanges/bybit/ratelimit_test.go
Normal file
35
exchanges/bybit/ratelimit_test.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package bybit
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
|
||||
)
|
||||
|
||||
func TestGetWSRateLimitEPLByCategory(t *testing.T) {
|
||||
t.Parallel()
|
||||
for _, tc := range []struct {
|
||||
category string
|
||||
expected request.EndpointLimit
|
||||
err error
|
||||
}{
|
||||
{"", 0, errUnknownCategory},
|
||||
{cSpot, wsOrderSpotEPL, nil},
|
||||
{cInverse, wsOrderInverseEPL, nil},
|
||||
{cLinear, wsOrderLinearEPL, nil},
|
||||
{cOption, wsOrderOptionsEPL, nil},
|
||||
} {
|
||||
t.Run(tc.category, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
actual, err := getWSRateLimitEPLByCategory(tc.category)
|
||||
if tc.err != nil {
|
||||
require.ErrorIs(t, err, tc.err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.expected, actual)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ func (e *Exchange) handleSpotSubscription(ctx context.Context, conn websocket.Co
|
||||
return err
|
||||
}
|
||||
if !resp.Success {
|
||||
return fmt.Errorf("%s with request ID %s msg: %s", resp.Operation, resp.RequestID, resp.RetMsg)
|
||||
return fmt.Errorf("%s with request ID %s msg: %s", resp.Operation, resp.RequestID, resp.ReturnMessage)
|
||||
}
|
||||
if operation == "unsubscribe" {
|
||||
err = e.Websocket.RemoveSubscriptions(conn, payload.associatedSubs...)
|
||||
|
||||
10
exchanges/bybit/unmarshal.go
Normal file
10
exchanges/bybit/unmarshal.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package bybit
|
||||
|
||||
import (
|
||||
"github.com/thrasher-corp/gocryptotrader/encoding/json"
|
||||
)
|
||||
|
||||
// UnmarshalJSON implements the json.Unmarshaler interface for KlineItem
|
||||
func (k *KlineItem) UnmarshalJSON(data []byte) error {
|
||||
return json.Unmarshal(data, &[7]any{&k.StartTime, &k.Open, &k.High, &k.Low, &k.Close, &k.TradeVolume, &k.Turnover})
|
||||
}
|
||||
24
exchanges/bybit/unmarshal_test.go
Normal file
24
exchanges/bybit/unmarshal_test.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package bybit
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/thrasher-corp/gocryptotrader/types"
|
||||
)
|
||||
|
||||
func TestUnmarshalJSONKlineItem(t *testing.T) {
|
||||
t.Parallel()
|
||||
ki := &KlineItem{}
|
||||
err := ki.UnmarshalJSON([]byte(`["1691905800000","0.000301","0.0003015","0.0002995","0.0003","213303600","64084.7623"]`))
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, time.UnixMilli(1691905800000), ki.StartTime.Time())
|
||||
require.Equal(t, types.Number(0.000301), ki.Open)
|
||||
require.Equal(t, types.Number(0.0003015), ki.High)
|
||||
require.Equal(t, types.Number(0.0002995), ki.Low)
|
||||
require.Equal(t, types.Number(0.0003), ki.Close)
|
||||
require.Equal(t, types.Number(213303600), ki.TradeVolume)
|
||||
require.Equal(t, types.Number(64084.7623), ki.Turnover)
|
||||
}
|
||||
93
exchanges/bybit/validate.go
Normal file
93
exchanges/bybit/validate.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package bybit
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"github.com/thrasher-corp/gocryptotrader/currency"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchange/order/limits"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
|
||||
)
|
||||
|
||||
var (
|
||||
validSides = []string{sideBuy, sideSell}
|
||||
validOrderTypes = []string{"Market", "Limit"}
|
||||
validOrderFilters = []string{"Order", "tpslOrder", "StopOrder"}
|
||||
validTriggerPrices = []string{"", "LastPrice", "IndexPrice", "MarkPrice"}
|
||||
)
|
||||
|
||||
// Validate checks the input parameters and returns an error if they are invalid.
|
||||
func (r *PlaceOrderRequest) Validate() error {
|
||||
if err := isValidCategory(r.Category); err != nil {
|
||||
return err
|
||||
}
|
||||
if r.Symbol.IsEmpty() {
|
||||
return currency.ErrCurrencyPairEmpty
|
||||
}
|
||||
if r.EnableBorrow {
|
||||
r.IsLeverage = 1
|
||||
}
|
||||
if !slices.Contains(validSides, r.Side) {
|
||||
return fmt.Errorf("%w: %q", order.ErrSideIsInvalid, r.Side)
|
||||
}
|
||||
if !slices.Contains(validOrderTypes, r.OrderType) {
|
||||
return fmt.Errorf("%w: %q", order.ErrTypeIsInvalid, r.OrderType)
|
||||
}
|
||||
if r.OrderQuantity <= 0 {
|
||||
return limits.ErrAmountBelowMin
|
||||
}
|
||||
switch r.TriggerDirection {
|
||||
case 0, 1, 2: // 0: None, 1: triggered when market price rises to triggerPrice, 2: triggered when market price falls to triggerPrice
|
||||
default:
|
||||
return fmt.Errorf("%w, triggerDirection: %d", errInvalidTriggerDirection, r.TriggerDirection)
|
||||
}
|
||||
if r.OrderFilter != "" {
|
||||
if r.Category != cSpot {
|
||||
return fmt.Errorf("%w, orderFilter is valid for 'spot' only", errInvalidCategory)
|
||||
}
|
||||
if !slices.Contains(validOrderFilters, r.OrderFilter) {
|
||||
return fmt.Errorf("%w, orderFilter=%s", errInvalidOrderFilter, r.OrderFilter)
|
||||
}
|
||||
}
|
||||
if !slices.Contains(validTriggerPrices, r.TriggerPriceType) {
|
||||
return errInvalidTriggerPriceType
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate checks the input parameters and returns an error if they are invalid
|
||||
func (r *AmendOrderRequest) Validate() error {
|
||||
if err := isValidCategory(r.Category); err != nil {
|
||||
return err
|
||||
}
|
||||
if r.Symbol.IsEmpty() {
|
||||
return currency.ErrCurrencyPairEmpty
|
||||
}
|
||||
if r.OrderID == "" && r.OrderLinkID == "" {
|
||||
return errEitherOrderIDOROrderLinkIDRequired
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate checks the input parameters and returns an error if they are invalid
|
||||
func (r *CancelOrderRequest) Validate() error {
|
||||
if err := isValidCategory(r.Category); err != nil {
|
||||
return err
|
||||
}
|
||||
if r.Symbol.IsEmpty() {
|
||||
return currency.ErrCurrencyPairEmpty
|
||||
}
|
||||
if r.OrderID == "" && r.OrderLinkID == "" {
|
||||
return errEitherOrderIDOROrderLinkIDRequired
|
||||
}
|
||||
if r.OrderFilter != "" {
|
||||
if r.Category != cSpot {
|
||||
return fmt.Errorf("%w, orderFilter is valid for 'spot' only", errInvalidCategory)
|
||||
}
|
||||
if !slices.Contains(validOrderFilters, r.OrderFilter) {
|
||||
return fmt.Errorf("%w, orderFilter=%s", errInvalidOrderFilter, r.OrderFilter)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
195
exchanges/bybit/validate_test.go
Normal file
195
exchanges/bybit/validate_test.go
Normal file
@@ -0,0 +1,195 @@
|
||||
package bybit
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/thrasher-corp/gocryptotrader/currency"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchange/order/limits"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
|
||||
)
|
||||
|
||||
func TestValidatePlaceOrderRequest(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, tc := range []struct {
|
||||
params PlaceOrderRequest
|
||||
err error
|
||||
}{
|
||||
{err: errCategoryNotSet},
|
||||
{params: PlaceOrderRequest{Category: cSpot}, err: currency.ErrCurrencyPairEmpty},
|
||||
{
|
||||
params: PlaceOrderRequest{Category: cSpot, Symbol: currency.NewBTCUSDT(), EnableBorrow: true},
|
||||
err: order.ErrSideIsInvalid,
|
||||
},
|
||||
{
|
||||
params: PlaceOrderRequest{
|
||||
Category: cSpot,
|
||||
Symbol: currency.NewBTCUSDT(),
|
||||
EnableBorrow: true,
|
||||
Side: sideBuy,
|
||||
},
|
||||
err: order.ErrTypeIsInvalid,
|
||||
},
|
||||
{
|
||||
params: PlaceOrderRequest{
|
||||
Category: cSpot,
|
||||
Symbol: currency.NewBTCUSDT(),
|
||||
EnableBorrow: true,
|
||||
Side: sideBuy,
|
||||
OrderType: orderTypeToString(order.Limit),
|
||||
},
|
||||
err: limits.ErrAmountBelowMin,
|
||||
},
|
||||
{
|
||||
params: PlaceOrderRequest{
|
||||
Category: cSpot,
|
||||
Symbol: currency.NewBTCUSDT(),
|
||||
EnableBorrow: true,
|
||||
Side: sideBuy,
|
||||
OrderType: orderTypeToString(order.Limit),
|
||||
OrderQuantity: 0.0001,
|
||||
TriggerDirection: 69,
|
||||
},
|
||||
err: errInvalidTriggerDirection,
|
||||
},
|
||||
{
|
||||
params: PlaceOrderRequest{
|
||||
Category: cInverse,
|
||||
Symbol: currency.NewBTCUSDT(),
|
||||
EnableBorrow: true,
|
||||
Side: sideBuy,
|
||||
OrderType: orderTypeToString(order.Limit),
|
||||
OrderQuantity: 0.0001,
|
||||
OrderFilter: "dodgy",
|
||||
},
|
||||
err: errInvalidCategory,
|
||||
},
|
||||
{
|
||||
params: PlaceOrderRequest{
|
||||
Category: cSpot,
|
||||
Symbol: currency.NewBTCUSDT(),
|
||||
EnableBorrow: true,
|
||||
Side: sideBuy,
|
||||
OrderType: orderTypeToString(order.Limit),
|
||||
OrderQuantity: 0.0001,
|
||||
OrderFilter: "dodgy",
|
||||
},
|
||||
err: errInvalidOrderFilter,
|
||||
},
|
||||
{
|
||||
params: PlaceOrderRequest{
|
||||
Category: cSpot,
|
||||
Symbol: currency.NewBTCUSDT(),
|
||||
EnableBorrow: true,
|
||||
Side: sideBuy,
|
||||
OrderType: orderTypeToString(order.Limit),
|
||||
OrderQuantity: 0.0001,
|
||||
TriggerPriceType: "dodgy",
|
||||
},
|
||||
err: errInvalidTriggerPriceType,
|
||||
},
|
||||
{
|
||||
params: PlaceOrderRequest{
|
||||
Category: cSpot,
|
||||
Symbol: currency.NewBTCUSDT(),
|
||||
EnableBorrow: true,
|
||||
Side: sideBuy,
|
||||
OrderType: orderTypeToString(order.Limit),
|
||||
OrderQuantity: 0.0001,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
} {
|
||||
if tc.err != nil {
|
||||
require.ErrorIs(t, tc.params.Validate(), tc.err)
|
||||
continue
|
||||
}
|
||||
require.NoError(t, tc.params.Validate())
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateAmendOrderRequest(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, tc := range []struct {
|
||||
params AmendOrderRequest
|
||||
err error
|
||||
}{
|
||||
{err: errCategoryNotSet},
|
||||
{params: AmendOrderRequest{Category: cSpot}, err: currency.ErrCurrencyPairEmpty},
|
||||
{
|
||||
params: AmendOrderRequest{
|
||||
Category: cSpot,
|
||||
Symbol: currency.NewBTCUSDT(),
|
||||
},
|
||||
err: errEitherOrderIDOROrderLinkIDRequired,
|
||||
},
|
||||
{
|
||||
params: AmendOrderRequest{
|
||||
Category: cSpot,
|
||||
Symbol: currency.NewBTCUSDT(),
|
||||
OrderID: "69420",
|
||||
TPSLMode: "TP",
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
} {
|
||||
if tc.err != nil {
|
||||
require.ErrorIs(t, tc.params.Validate(), tc.err)
|
||||
continue
|
||||
}
|
||||
require.NoError(t, tc.params.Validate())
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateCancelOrderRequest(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, tc := range []struct {
|
||||
params CancelOrderRequest
|
||||
err error
|
||||
}{
|
||||
{err: errCategoryNotSet},
|
||||
{params: CancelOrderRequest{Category: cSpot}, err: currency.ErrCurrencyPairEmpty},
|
||||
{
|
||||
params: CancelOrderRequest{
|
||||
Category: cSpot,
|
||||
Symbol: currency.NewBTCUSDT(),
|
||||
},
|
||||
err: errEitherOrderIDOROrderLinkIDRequired,
|
||||
},
|
||||
{
|
||||
params: CancelOrderRequest{
|
||||
Category: cLinear,
|
||||
Symbol: currency.NewBTCUSDT(),
|
||||
OrderID: "69420",
|
||||
OrderFilter: "dodgy",
|
||||
},
|
||||
err: errInvalidCategory,
|
||||
},
|
||||
{
|
||||
params: CancelOrderRequest{
|
||||
Category: cSpot,
|
||||
Symbol: currency.NewBTCUSDT(),
|
||||
OrderID: "69420",
|
||||
OrderFilter: "dodgy",
|
||||
},
|
||||
err: errInvalidOrderFilter,
|
||||
},
|
||||
{
|
||||
params: CancelOrderRequest{
|
||||
Category: cSpot,
|
||||
Symbol: currency.NewBTCUSDT(),
|
||||
OrderID: "69420",
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
} {
|
||||
if tc.err != nil {
|
||||
require.ErrorIs(t, tc.params.Validate(), tc.err)
|
||||
continue
|
||||
}
|
||||
require.NoError(t, tc.params.Validate())
|
||||
}
|
||||
}
|
||||
@@ -1377,6 +1377,8 @@ func (u URL) String() string {
|
||||
return websocketUSDCMarginedURL
|
||||
case WebsocketOptions:
|
||||
return websocketOptionsURL
|
||||
case WebsocketTrade:
|
||||
return websocketTradeURL
|
||||
case WebsocketPrivate:
|
||||
return websocketPrivateURL
|
||||
case WebsocketSpotSupplementary:
|
||||
@@ -1425,6 +1427,8 @@ func getURLTypeFromString(ep string) (URL, error) {
|
||||
return WebsocketUSDCMargined, nil
|
||||
case websocketOptionsURL:
|
||||
return WebsocketOptions, nil
|
||||
case websocketTradeURL:
|
||||
return WebsocketTrade, nil
|
||||
case websocketPrivateURL:
|
||||
return WebsocketPrivate, nil
|
||||
case websocketSpotSupplementaryURL:
|
||||
@@ -1968,6 +1972,16 @@ func (*Base) WebsocketSubmitOrders(context.Context, []*order.Submit) (responses
|
||||
return nil, common.ErrFunctionNotSupported
|
||||
}
|
||||
|
||||
// WebsocketModifyOrder modifies an order via the websocket connection
|
||||
func (*Base) WebsocketModifyOrder(context.Context, *order.Modify) (*order.ModifyResponse, error) {
|
||||
return nil, common.ErrFunctionNotSupported
|
||||
}
|
||||
|
||||
// WebsocketCancelOrder cancels an order via the websocket connection
|
||||
func (*Base) WebsocketCancelOrder(context.Context, *order.Cancel) error {
|
||||
return common.ErrFunctionNotSupported
|
||||
}
|
||||
|
||||
// MessageID returns a universally unique id using UUID V7
|
||||
// In the future additional params may be added to method signature to provide context for the message id for overriding exchange implementations
|
||||
func (b *Base) MessageID() string {
|
||||
|
||||
@@ -1665,6 +1665,7 @@ func TestString(t *testing.T) {
|
||||
{WebsocketUSDTMargined, websocketUSDTMarginedURL},
|
||||
{WebsocketUSDCMargined, websocketUSDCMarginedURL},
|
||||
{WebsocketOptions, websocketOptionsURL},
|
||||
{WebsocketTrade, websocketTradeURL},
|
||||
{WebsocketPrivate, websocketPrivateURL},
|
||||
{WebsocketSpotSupplementary, websocketSpotSupplementaryURL},
|
||||
{ChainAnalysis, chainAnalysisURL},
|
||||
@@ -1828,6 +1829,7 @@ func TestGetGetURLTypeFromString(t *testing.T) {
|
||||
{Endpoint: websocketUSDTMarginedURL, Expected: WebsocketUSDTMargined},
|
||||
{Endpoint: websocketUSDCMarginedURL, Expected: WebsocketUSDCMargined},
|
||||
{Endpoint: websocketOptionsURL, Expected: WebsocketOptions},
|
||||
{Endpoint: websocketTradeURL, Expected: WebsocketTrade},
|
||||
{Endpoint: websocketPrivateURL, Expected: WebsocketPrivate},
|
||||
{Endpoint: websocketSpotSupplementaryURL, Expected: WebsocketSpotSupplementary},
|
||||
{Endpoint: chainAnalysisURL, Expected: ChainAnalysis},
|
||||
@@ -2877,6 +2879,18 @@ func TestWebsocketSubmitOrders(t *testing.T) {
|
||||
require.ErrorIs(t, err, common.ErrFunctionNotSupported)
|
||||
}
|
||||
|
||||
func TestWebsocketModifyOrder(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, err := (&Base{}).WebsocketModifyOrder(t.Context(), nil)
|
||||
require.ErrorIs(t, err, common.ErrFunctionNotSupported)
|
||||
}
|
||||
|
||||
func TestWebsocketCancelOrder(t *testing.T) {
|
||||
t.Parallel()
|
||||
err := (&Base{}).WebsocketCancelOrder(t.Context(), nil)
|
||||
require.ErrorIs(t, err, common.ErrFunctionNotSupported)
|
||||
}
|
||||
|
||||
func TestMessageID(t *testing.T) {
|
||||
t.Parallel()
|
||||
id := (new(Base)).MessageID()
|
||||
|
||||
@@ -276,6 +276,7 @@ const (
|
||||
WebsocketUSDTMargined
|
||||
WebsocketUSDCMargined
|
||||
WebsocketOptions
|
||||
WebsocketTrade
|
||||
WebsocketPrivate
|
||||
WebsocketSpotSupplementary
|
||||
ChainAnalysis
|
||||
@@ -297,6 +298,7 @@ const (
|
||||
websocketUSDTMarginedURL = "WebsocketUSDTMarginedURL"
|
||||
websocketUSDCMarginedURL = "WebsocketUSDCMarginedURL"
|
||||
websocketOptionsURL = "WebsocketOptionsURL"
|
||||
websocketTradeURL = "WebsocketTradeURL"
|
||||
websocketPrivateURL = "WebsocketPrivateURL"
|
||||
websocketSpotSupplementaryURL = "WebsocketSpotSupplementaryURL"
|
||||
chainAnalysisURL = "ChainAnalysisURL"
|
||||
@@ -320,6 +322,7 @@ var keyURLs = []URL{
|
||||
WebsocketUSDTMargined,
|
||||
WebsocketUSDCMargined,
|
||||
WebsocketOptions,
|
||||
WebsocketTrade,
|
||||
WebsocketPrivate,
|
||||
WebsocketSpotSupplementary,
|
||||
ChainAnalysis,
|
||||
|
||||
@@ -57,8 +57,8 @@ func (e *Exchange) SetDefaults() {
|
||||
currency.NewCode("MBABYDOGE"): currency.BABYDOGE,
|
||||
}),
|
||||
TradingRequirements: protocol.TradingRequirements{
|
||||
SpotMarketOrderAmountPurchaseQuotationOnly: true,
|
||||
SpotMarketOrderAmountSellBaseOnly: true,
|
||||
SpotMarketBuyQuotation: true,
|
||||
SpotMarketSellBase: true,
|
||||
},
|
||||
Supports: exchange.FeaturesSupported{
|
||||
REST: true,
|
||||
|
||||
@@ -135,6 +135,8 @@ type OrderManagement interface {
|
||||
GetOrderHistory(ctx context.Context, getOrdersRequest *order.MultiOrderRequest) (order.FilteredOrders, error)
|
||||
WebsocketSubmitOrder(ctx context.Context, s *order.Submit) (*order.SubmitResponse, error)
|
||||
WebsocketSubmitOrders(ctx context.Context, orders []*order.Submit) (responses []*order.SubmitResponse, err error)
|
||||
WebsocketModifyOrder(ctx context.Context, action *order.Modify) (*order.ModifyResponse, error)
|
||||
WebsocketCancelOrder(ctx context.Context, ord *order.Cancel) error
|
||||
}
|
||||
|
||||
// CurrencyStateManagement defines functionality for currency state management
|
||||
|
||||
@@ -240,9 +240,9 @@ func TestSubmitValidate(t *testing.T) {
|
||||
t.Run(strconv.Itoa(x), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
requirements := protocol.TradingRequirements{
|
||||
SpotMarketOrderAmountPurchaseQuotationOnly: tc.HasToPurchaseWithQuoteAmountSet,
|
||||
SpotMarketOrderAmountSellBaseOnly: tc.HasToSellWithBaseAmountSet,
|
||||
ClientOrderID: tc.RequiresID,
|
||||
SpotMarketBuyQuotation: tc.HasToPurchaseWithQuoteAmountSet,
|
||||
SpotMarketSellBase: tc.HasToSellWithBaseAmountSet,
|
||||
ClientOrderID: tc.RequiresID,
|
||||
}
|
||||
err := tc.Submit.Validate(requirements, tc.ValidOpts)
|
||||
assert.ErrorIs(t, err, tc.ExpectedErr)
|
||||
@@ -1693,14 +1693,14 @@ func TestGetTradeAmount(t *testing.T) {
|
||||
s = &Submit{Amount: baseAmount, QuoteAmount: quoteAmount}
|
||||
// below will default to base amount with nothing set
|
||||
require.Equal(t, baseAmount, s.GetTradeAmount(protocol.TradingRequirements{}))
|
||||
require.Equal(t, baseAmount, s.GetTradeAmount(protocol.TradingRequirements{SpotMarketOrderAmountPurchaseQuotationOnly: true}))
|
||||
require.Equal(t, baseAmount, s.GetTradeAmount(protocol.TradingRequirements{SpotMarketBuyQuotation: true}))
|
||||
s.AssetType = asset.Spot
|
||||
s.Type = Market
|
||||
s.Side = Buy
|
||||
require.Equal(t, quoteAmount, s.GetTradeAmount(protocol.TradingRequirements{SpotMarketOrderAmountPurchaseQuotationOnly: true}))
|
||||
require.Equal(t, baseAmount, s.GetTradeAmount(protocol.TradingRequirements{SpotMarketOrderAmountSellBaseOnly: true}))
|
||||
require.Equal(t, quoteAmount, s.GetTradeAmount(protocol.TradingRequirements{SpotMarketBuyQuotation: true}))
|
||||
require.Equal(t, baseAmount, s.GetTradeAmount(protocol.TradingRequirements{SpotMarketSellBase: true}))
|
||||
s.Side = Sell
|
||||
require.Equal(t, baseAmount, s.GetTradeAmount(protocol.TradingRequirements{SpotMarketOrderAmountSellBaseOnly: true}))
|
||||
require.Equal(t, baseAmount, s.GetTradeAmount(protocol.TradingRequirements{SpotMarketSellBase: true}))
|
||||
}
|
||||
|
||||
func TestStringToTrackingMode(t *testing.T) {
|
||||
|
||||
@@ -110,11 +110,11 @@ func (s *Submit) Validate(requirements protocol.TradingRequirements, opt ...vali
|
||||
return fmt.Errorf("submit validation error %w, client order ID must be set to satisfy submission requirements", ErrClientOrderIDMustBeSet)
|
||||
}
|
||||
|
||||
if requirements.SpotMarketOrderAmountPurchaseQuotationOnly && s.QuoteAmount == 0 && s.Type == Market && s.AssetType == asset.Spot && s.Side.IsLong() {
|
||||
if requirements.SpotMarketBuyQuotation && s.QuoteAmount == 0 && s.Type == Market && s.AssetType == asset.Spot && s.Side.IsLong() {
|
||||
return fmt.Errorf("submit validation error %w, quote amount to be sold must be set to 'QuoteAmount' field to satisfy trading requirements", ErrAmountMustBeSet)
|
||||
}
|
||||
|
||||
if requirements.SpotMarketOrderAmountSellBaseOnly && s.Amount == 0 && s.Type == Market && s.AssetType == asset.Spot && s.Side.IsShort() {
|
||||
if requirements.SpotMarketSellBase && s.Amount == 0 && s.Type == Market && s.AssetType == asset.Spot && s.Side.IsShort() {
|
||||
return fmt.Errorf("submit validation error %w, base amount being sold must be set to 'Amount' field to satisfy trading requirements", ErrAmountMustBeSet)
|
||||
}
|
||||
|
||||
@@ -137,9 +137,9 @@ func (s *Submit) GetTradeAmount(tr protocol.TradingRequirements) float64 {
|
||||
return 0
|
||||
}
|
||||
switch {
|
||||
case tr.SpotMarketOrderAmountPurchaseQuotationOnly && s.AssetType == asset.Spot && s.Type == Market && s.Side.IsLong():
|
||||
case tr.SpotMarketBuyQuotation && s.AssetType == asset.Spot && s.Type == Market && s.Side.IsLong():
|
||||
return s.QuoteAmount
|
||||
case tr.SpotMarketOrderAmountSellBaseOnly && s.AssetType == asset.Spot && s.Type == Market && s.Side.IsShort():
|
||||
case tr.SpotMarketSellBase && s.AssetType == asset.Spot && s.Type == Market && s.Side.IsShort():
|
||||
return s.Amount
|
||||
}
|
||||
return s.Amount
|
||||
|
||||
@@ -50,18 +50,10 @@ type Features struct {
|
||||
|
||||
// TradingRequirements defines the requirements for trading on the exchange.
|
||||
type TradingRequirements struct {
|
||||
// SpotMarketOrderAmountPurchaseQuotationOnly requires the amount to be in
|
||||
// quote currency or what is to be sold for when you purchase base currency.
|
||||
// For example, long BTC-USD, the quotation amount is USD.
|
||||
// NOTE: Due to an exchange's matching engine process, the base amount
|
||||
// acquired may vary from what is intended due to price fluctuations and
|
||||
// liquidity on the books. Care must be taken when implementing a market
|
||||
// neutral strategy.
|
||||
SpotMarketOrderAmountPurchaseQuotationOnly bool
|
||||
// SpotMarketOrderAmountSellBaseOnly requires the amount to be in the
|
||||
// base currency or what is intended to be purchased. For example, short
|
||||
// BTC-USD, the base amount is BTC.
|
||||
SpotMarketOrderAmountSellBaseOnly bool
|
||||
// SpotMarketBuyQuotation requires the amount to be in quote currency
|
||||
SpotMarketBuyQuotation bool
|
||||
// SpotMarketSellBase requires the amount to be in the base currency
|
||||
SpotMarketSellBase bool
|
||||
// ClientOrderID is a unique identifier for the order that is generated by
|
||||
// the client and is required for order submission.
|
||||
ClientOrderID bool
|
||||
|
||||
@@ -20,6 +20,9 @@ var (
|
||||
errSpecificRateLimiterIsNil = errors.New("specific rate limiter is nil")
|
||||
)
|
||||
|
||||
// RateLimitNotRequired is a no-op rate limiter
|
||||
var RateLimitNotRequired *RateLimiterWithWeight
|
||||
|
||||
// Const here define individual functionality sub types for rate limiting
|
||||
const (
|
||||
Unset EndpointLimit = iota
|
||||
|
||||
10
testdata/configtest.json
vendored
10
testdata/configtest.json
vendored
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user