Skip to content

Commit

Permalink
Bitget websocket beginnings
Browse files Browse the repository at this point in the history
  • Loading branch information
cranktakular committed Sep 3, 2024
1 parent deff267 commit 63157c6
Show file tree
Hide file tree
Showing 6 changed files with 281 additions and 34 deletions.
34 changes: 32 additions & 2 deletions exchanges/bitget/bitget.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ const (
bitgetSubaccountAssets2 = "/sub-account-assets"
bitgetOpenCount = "/open-count"
bitgetSetLeverage = "/set-leverage"
bitgetSetAutoMargin = "/set-auto-margin"
bitgetSetMargin = "/set-margin"
bitgetSetMarginMode = "/set-margin-mode"
bitgetSetPositionMode = "/set-position-mode"
Expand Down Expand Up @@ -200,6 +201,12 @@ const (
bitgetDebts = "/debts"
bitgetReduces = "/reduces"

// Websocket endpoints
// Unauthenticated
bitgetTickerChannel = "ticker"

// Authenticated

errIntervalNotSupported = "interval not supported"
)

Expand Down Expand Up @@ -1129,8 +1136,8 @@ func (bi *Bitget) BatchCancelOrders(ctx context.Context, pair string, orderIDs [
&resp)
}

// CancelOrderBySymbol cancels orders for a given symbol. Doesn't return information on failures/successes
func (bi *Bitget) CancelOrderBySymbol(ctx context.Context, pair string) (*SymbolResp, error) {
// CancelOrdersBySymbol cancels orders for a given symbol. Doesn't return information on failures/successes
func (bi *Bitget) CancelOrdersBySymbol(ctx context.Context, pair string) (*SymbolResp, error) {
if pair == "" {
return nil, errPairEmpty
}
Expand Down Expand Up @@ -2116,6 +2123,29 @@ func (bi *Bitget) ChangeLeverage(ctx context.Context, pair, productType, marginC
&resp)
}

// AdjustIsolatedAutoMargin adjusts the auto margin for a specified isolated margin account
func (bi *Bitget) AdjustIsolatedAutoMargin(ctx context.Context, pair, marginCoin, holdSide string, autoMargin bool, amount float64) error {
if pair == "" {
return errPairEmpty
}
if marginCoin == "" {
return errMarginCoinEmpty
}
req := map[string]interface{}{
"symbol": pair,
"marginCoin": marginCoin,
"holdSide": holdSide,
"amount": strconv.FormatFloat(amount, 'f', -1, 64),
}
if autoMargin {
req["autoMargin"] = "on"
} else {
req["autoMargin"] = "off"
}
path := bitgetMix + bitgetAccount + bitgetSetAutoMargin
return bi.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, Rate5, http.MethodPost, path, nil, req, nil)
}

// AdjustMargin adds or subtracts margin from a position
func (bi *Bitget) AdjustMargin(ctx context.Context, pair, productType, marginCoin, holdSide string, amount float64) error {
if pair == "" {
Expand Down
56 changes: 42 additions & 14 deletions exchanges/bitget/bitget_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package bitget

import (
"context"
"fmt"
"log"
"net/url"
"os"
Expand All @@ -13,6 +14,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/common/key"
"github.com/thrasher-corp/gocryptotrader/config"
"github.com/thrasher-corp/gocryptotrader/currency"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
Expand Down Expand Up @@ -640,7 +642,7 @@ func TestBatchCancelOrders(t *testing.T) {

func TestCancelOrderBySymbol(t *testing.T) {
t.Parallel()
testGetOneArg(t, bi.CancelOrderBySymbol, "", testPair.String(), errPairEmpty, true, true,
testGetOneArg(t, bi.CancelOrdersBySymbol, "", testPair.String(), errPairEmpty, true, true,
canManipulateRealOrders)
}

Expand Down Expand Up @@ -1130,6 +1132,21 @@ func TestChangeLeverage(t *testing.T) {
assert.NotEmpty(t, resp.Data)
}

func TestAdjustIsolatedAutoMargin(t *testing.T) {
t.Parallel()
err := bi.AdjustIsolatedAutoMargin(context.Background(), "", "", "", false, 0)
assert.ErrorIs(t, err, errPairEmpty)
err = bi.AdjustIsolatedAutoMargin(context.Background(), "meow", "", "", false, 0)
assert.ErrorIs(t, err, errMarginCoinEmpty)
sharedtestvalues.SkipTestIfCredentialsUnset(t, bi, canManipulateRealOrders)
err = bi.AdjustIsolatedAutoMargin(context.Background(), testPair2.String(), testFiat2.String(), "long",
false, 0)
assert.NoError(t, err)
err = bi.AdjustIsolatedAutoMargin(context.Background(), testPair2.String(), testFiat2.String(), "long",
true, 0.01)
assert.NoError(t, err)
}

func TestAdjustMargin(t *testing.T) {
t.Parallel()
err := bi.AdjustMargin(context.Background(), "", "", "", "", 0)
Expand Down Expand Up @@ -1582,6 +1599,22 @@ func TestGetHistoricalTriggerFuturesOrders(t *testing.T) {
func TestGetSupportedCurrencies(t *testing.T) {
t.Parallel()
testGetNoArgs(t, bi.GetSupportedCurrencies)
assets := []asset.Item{asset.Spot, asset.Futures, asset.Margin}
var curMax string
for i := range assets {
resp, err := bi.FetchTradablePairs(context.Background(), assets[i])
require.NoError(t, err)
for j := range resp {
if len(resp[j].String()) > len(curMax) {
curMax = resp[j].String()
}
}
}
fmt.Print(curMax)
}

func TestLen(t *testing.T) {
fmt.Print(int(1)/int(47), int(50)/47, int(94)/47)
}

func TestGetCrossBorrowHistory(t *testing.T) {
Expand Down Expand Up @@ -2914,7 +2947,14 @@ func TestGetLeverage(t *testing.T) {
assert.NoError(t, err)
}

// The following 3 tests aren't parallel due to collisions with each other, and some other plan order-related tests
func TestGetOpenInterest(t *testing.T) {
t.Parallel()
_, err := bi.GetOpenInterest(context.Background(), key.PairAsset{Base: testCrypto.Item, Quote: testFiat.Item,
Asset: asset.Futures})
assert.NoError(t, err)
}

// The following 18 tests aren't parallel due to collisions with each other, and some other tests
func TestModifyPlanSpotOrder(t *testing.T) {
_, err := bi.ModifyPlanSpotOrder(context.Background(), 0, "", "", 0, 0, 0)
assert.ErrorIs(t, err, errOrderClientEmpty)
Expand Down Expand Up @@ -2989,8 +3029,6 @@ func TestModifyOrder(t *testing.T) {
}

func TestCommitConversion(t *testing.T) {
// In a separate parallel batch due to collision with TestGetQuotedPrice
t.Parallel()
_, err := bi.CommitConversion(context.Background(), "", "", "", 0, 0, 0)
assert.ErrorIs(t, err, errCurrencyEmpty)
_, err = bi.CommitConversion(context.Background(), testCrypto.String(), testFiat.String(), "", 0, 0, 0)
Expand All @@ -3008,8 +3046,6 @@ func TestCommitConversion(t *testing.T) {
}

func TestCancelTriggerFuturesOrders(t *testing.T) {
// In a separate parallel batch due to collisions with TestModifyTPSLFuturesOrder and TestModifyTriggerFuturesOrder
t.Parallel()
var ordList []OrderIDStruct
_, err := bi.CancelTriggerFuturesOrders(context.Background(), ordList, "", "", "", "")
assert.ErrorIs(t, err, errProductTypeEmpty)
Expand All @@ -3023,8 +3059,6 @@ func TestCancelTriggerFuturesOrders(t *testing.T) {
}

func TestRepayLoan(t *testing.T) {
// In a separate parallel batch due to a collision with ModifyPledgeRate
t.Parallel()
_, err := bi.RepayLoan(context.Background(), 0, 0, false, false)
assert.ErrorIs(t, err, errOrderIDEmpty)
_, err = bi.RepayLoan(context.Background(), 1, 0, false, false)
Expand All @@ -3040,8 +3074,6 @@ func TestRepayLoan(t *testing.T) {
_, err = bi.RepayLoan(context.Background(), resp.Data[0].OrderID, 0, true, true)
assert.NoError(t, err)
}

// The following 7 tests aren't parallel due to collisions with each other, and some other futures-related tests
func TestModifyFuturesOrder(t *testing.T) {
_, err := bi.ModifyFuturesOrder(context.Background(), 0, "", "", "", "", 0, 0, 0, 0)
assert.ErrorIs(t, err, errOrderClientEmpty)
Expand Down Expand Up @@ -3166,8 +3198,6 @@ func TestCancelAllOrders(t *testing.T) {
_, err = bi.CancelAllOrders(context.Background(), ord)
assert.NoError(t, err)
}

// The following 3 tests aren't parallel due to collisions with each other, and some other cross-related tests
func TestCancelCrossOrder(t *testing.T) {
_, err := bi.CancelCrossOrder(context.Background(), "", "", 0)
assert.ErrorIs(t, err, errPairEmpty)
Expand Down Expand Up @@ -3245,8 +3275,6 @@ func TestCancelBatchOrders(t *testing.T) {
_, err = bi.CancelBatchOrders(context.Background(), orders)
assert.NoError(t, err)
}

// The following 2 tests aren't parallel due to collisions with each other, and some other isolated-related tests
func TestCancelIsolatedOrder(t *testing.T) {
_, err := bi.CancelIsolatedOrder(context.Background(), "", "", 0)
assert.ErrorIs(t, err, errPairEmpty)
Expand Down
41 changes: 40 additions & 1 deletion exchanges/bitget/bitget_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -1112,7 +1112,7 @@ const (
CallModeMark
)

// OpenInterestResp contains information on open positions
// OpenPositionResp contains information on open positions
type OpenPositionsResp struct {
Data struct {
OpenInterestList []struct {
Expand Down Expand Up @@ -2430,3 +2430,42 @@ type LiquidRecs struct {
RepayLoanAmount float64 `json:"repayLoanAmount,string"`
} `json:"data"`
}

// WsResponse contains information on a websocket response
type WsResponse struct {
Event string `json:"event"`
Code int `json:"code,string"`
Message string `json:"msg"`
Arg struct {
InstrumentType string `json:"instType"`
Channel string `json:"channel"`
InstrumentID string `json:"instId"`
} `json:"arg"`
Action string `json:"action"`
Data json.RawMessage `json:"data"`
Timestamp UnixTimestamp `json:"ts"`
}

// WsArgument contains information used in a websocket request
type WsArgument struct {
InstrumentType string `json:"instType"`
Channel string `json:"channel"`
InstrumentID string `json:"instId"`
}

// WsRequest contains information on a websocket request
type WsRequest struct {
Operation string `json:"op"`
Arguments []WsArgument `json:"args"`
}

// WsLogin contains information on a websocket login request
type WsLogin struct {
Operation string `json:"op"`
Arguments []struct {
APIKey string `json:"apiKey"`
Passphrase string `json:"passphrase"`
Timestamp string `json:"timestamp"`
Signature string `json:"sign"`
} `json:"args"`
}
128 changes: 128 additions & 0 deletions exchanges/bitget/bitget_websocket.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package bitget

import (
"encoding/json"
"fmt"
"net/http"

"github.com/gorilla/websocket"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/stream"
"github.com/thrasher-corp/gocryptotrader/exchanges/subscription"
"github.com/thrasher-corp/gocryptotrader/log"
)

const (
bitgetPublicWSURL = "wss://ws.bitget.com/v2/ws/public/"
bitgetPrivateWSURL = "wss://ws.bitget.com/v2/ws/private/"
)

// WsConnect connects to a websocket feed
func (bi *Bitget) WsConnect() error {
if !bi.Websocket.IsEnabled() || !bi.IsEnabled() {
return stream.ErrWebsocketNotEnabled
}
var dialer websocket.Dialer
err := bi.Websocket.Conn.Dial(&dialer, http.Header{})
if err != nil {
return err
}
if bi.Verbose {
log.Debugf(log.ExchangeSys, "%s connected to Websocket.\n", bi.Name)
}
bi.Websocket.Wg.Add(1)
go bi.wsReadData()
return nil
}

// wsReadData receives and passes on websocket messages for processing
func (bi *Bitget) wsReadData() {
defer bi.Websocket.Wg.Done()
for {
resp := bi.Websocket.Conn.ReadMessage()
if resp.Raw == nil {
return
}
err := bi.wsHandleData(resp.Raw)
if err != nil {
bi.Websocket.DataHandler <- err
}
}
}

func (bi *Bitget) wsHandleData(respRaw []byte) error {
var wsResponse WsResponse
err := json.Unmarshal(respRaw, &wsResponse)
if err != nil {
return err
}
switch wsResponse.Event {
case "pong":
if bi.Verbose {
log.Debugf(log.ExchangeSys, "%v - Websocket %v\n", bi.Name, wsResponse.Event)
}
case "subscribe":
if bi.Verbose {
log.Debugf(log.ExchangeSys, "%v - Websocket %v succeeded for %v\n", bi.Name, wsResponse.Event,
wsResponse.Arg)
}
case "error":
return fmt.Errorf("%v - Websocket error, code: %v message: %v", bi.Name, wsResponse.Code, wsResponse.Message)
default:
bi.Websocket.DataHandler <- stream.UnhandledMessageWarning{Message: bi.Name + stream.UnhandledMessage +
string(respRaw)}
}
return nil
}

func (bi *Bitget) generateDefaultSubscriptions() (subscription.List, error) {
channels := []string{bitgetTickerChannel}
enabledPairs, err := bi.GetEnabledPairs(asset.Spot)
if err != nil {
return nil, err
}
var subscriptions subscription.List
for i := range channels {
subscriptions = append(subscriptions, &subscription.Subscription{
Channel: channels[i],
Pairs: enabledPairs,
Asset: asset.Spot,
})
}
return subscriptions, nil
}

// Subscribe sends a websocket message to receive data from the channel
func (bi *Bitget) Subscribe(subs subscription.List) error {
baseReq := &WsRequest{
Operation: "subscribe",
}
for _, s := range subs {
for i := range s.Pairs {
baseReq.Arguments = append(baseReq.Arguments, WsArgument{
Channel: s.Channel,
InstrumentType: s.Asset.String(),
InstrumentID: s.Pairs[i].String(),
})
}
}
cap := (len(baseReq.Arguments) / 47)

Check failure on line 110 in exchanges/bitget/bitget_websocket.go

View workflow job for this annotation

GitHub Actions / lint

builtinShadow: shadowing of predeclared identifier: cap (gocritic)
reqSlice := make([]WsRequest, cap)
for i := 0; i < cap; i++ {
reqSlice[i].Operation = baseReq.Operation
if i == cap-1 {
reqSlice[i].Arguments = baseReq.Arguments[i*47:]
break
}
reqSlice[i].Arguments = baseReq.Arguments[i*47 : (i+1)*47]
}
for i := range reqSlice {
err := bi.Websocket.Conn.SendJSONMessage(reqSlice[i])
if err != nil {
}
}
return common.ErrNotYetImplemented
}

// SendJSONMessage sends a JSON message to the connected websocket
Loading

0 comments on commit 63157c6

Please sign in to comment.