Skip to content

Conversation

@cranktakular
Copy link
Collaborator

@cranktakular cranktakular commented Mar 10, 2025

PR Description

Implementing Bitget's V2 API, specifically the Common, Spot, Future, Margin, Earn, and Inst Loan sections.

In addition, does some smaller fixes across the codebase:

  • Exports more errors.
  • Lets types.Number support "null" values, interpreting them as 0.
  • Removes GetPositionSummary, as there's already GetFuturesPositionSummary, which fulfills the same functionality.

Type of change

Please delete options that are not relevant and add an x in [] as item is complete.

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

How has this been tested

Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration and also consider improving test coverage whilst working on a certain feature or package.

  • go test ./... -race
  • golangci-lint run
  • Test X

Checklist

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation and regenerated documentation via the documentation tool
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally and on Github Actions with my changes

Copy link
Collaborator

@shazbert shazbert left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very fine work. Some quick nits to get started.

AIDOGE = NewCode("AIDOGE")
PEPE = NewCode("PEPE")
USDCM = NewCode("USDCM")
SUSDT = NewCode("SUSDT")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is sus

// Public endpoints
bitgetPublic = "public/"
bitgetAnnouncements = "annoucements" // Misspelling of announcements
bitgetTime = "time"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See if you can consolidate this list to the function calls and remove the bitget prefix on the left over consts

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean by consolidating to function calls?

Removing the prefix will cause some of them to run into conflicts (i.e. time and ticker would conflict with the packages). I could keep prefixes there or add different suffixes, but this would total to a lot of work for a purely cosmetic change.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By consolidation I mean you add the string "/somepath" directly to the path if its not shared in different functions and which might reduce this list. Leave that up to you if you want to change the bitget prefix.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could definitely consolidate part of these, and the reason to split them out isn't too significant now (it was done so that I could identify new endpoints which share those). But it does seem like a fair bit of work (searching 180 different consts to see how many of them appear multiple times, and appropriately replacing those which only appear once) for minimal reward (editors of this file need to scroll through fewer lines).

I'd rather not change these, but if others disagree, I can.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that's fine keep as is. 👍

)

var (
errBusinessTypeEmpty = errors.New("businessType cannot be empty")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: seems like we might be able to have a common.ErrEmptyValue across this list and then wrap field to it but up to you.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you just mean, for most of these values, replacing them with something like = fmt.Errorf("%w: businessType", common.ErrEmptyValue") ?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah something like that but up to you if you want to do that.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather not unless others request it.

Copy link
Collaborator

@samuael samuael left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a cool PR. I have made a quick review. Will get back to it another round

}

if b.Asset.String() == "" {
return errAssetTypeNotSet
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you move this function to the assets folder?

// Public errors
var (
ErrOrderbookNotFound = errors.New("cannot find orderbook(s)")
ErrAssetTypeNotSet = errors.New("orderbook asset type not set")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this should be in this package

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it makes some sense; the orderbook's asset type isn't set, seems fine for the orderbook package.

But I wouldn't be too opposed to moving it.

case 'n', 't', 'f': // null, true, false
case 't', 'f': // true, false
return fmt.Errorf("%w: %s", errInvalidNumberValue, data)
case 'n': // null
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand why this is an exception since we only checked the first entry and could not be sure to be sure to see whether it is 'n' for 'null' or for 'north'

Copy link
Collaborator Author

@cranktakular cranktakular Oct 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since it's JSON, only certain values are allowed outside of quote marks. If a different JSON implementation is sending north, not included inside quote marks, it would not be conforming to standard (RFC8259, section 3).

Copy link
Collaborator

@samuael samuael left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just added few comments on top of what I did before.

params.Values.Set("status", status)
params.Values.Set("side", side)
params.Values.Set("language", "en-US")
if !cryptoCurrency.IsEmpty() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could group these optional variables check together. Just for consistency.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could, but it's aesthetic and can't easily be automatically detected, so I won't bother; I'll continue leaving them in the order the exchange provided them.

}

// GenerateDefaultSubscriptions generates default subscriptions
func (e *Exchange) generateDefaultSubscriptions() (subscription.List, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can modify this implementation by using the new exchange template implementation.

wg.Add(1)
go func(req WsRequest) {
defer wg.Done()
err := e.Websocket.Conn.SendJSONMessage(context.TODO(), rateSubscription, req)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if err := ...; err != nil {}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ran out of time to completely do this, but I've caught a lot of these. Will catch more next week.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got the rest of these.

wg.Add(1)
go func(req WsRequest) {
defer wg.Done()
err := e.Websocket.AuthConn.SendJSONMessage(context.TODO(), rateSubscription, req)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shorten here too


// AccountUpdateDataHandler
func (e *Exchange) accountUpdateDataHandler(wsResponse *WsResponse, respRaw []byte) error {
creds, err := e.GetCredentials(context.TODO())
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pass ctx from the calling instance or use context.Background()

Copy link
Collaborator

@samuael samuael left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not all of my last review are applied but I have added more

vals.Set("period", period)
}
path := bitgetMargin + bitgetMarket + bitgetIsolatedBorrowRate
var resp []BorrowRatioResp
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you change such occurrences into using a slice of pointers? that way it will become easier to loop over each elements without using indexing

Suggested change
var resp []BorrowRatioResp
var resp []*BorrowRatioResp

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean? It's slightly easier to loop over slices when they aren't of pointers since then you don't need to dereference it afterwards.

sli := []int64{1, 2, 3, 4}
	for _, b := range sli {
		fmt.Printf("%v\n", b)
	}

Compared to

i1, i2, i3, i4 := int64(1), int64(2), int64(3), int64(4)
	sli := []*int64{&i1, &i2, &i3, &i4}
	for _, b := range sli {
		fmt.Printf("%v\n", *b)
	}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are not supposed to de-reference this since you only use it to access the data inside it. When you use list of pointers, you can directly use the value from the loop to access it instead of using a slice indexing(result[i]) to load the value at that index.

vals.Set("symbol", pair.String())
vals.Set("period", period)
path := bitgetMix + bitgetMarket + bitgetLongShort
var resp []RatioResp
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here

vals := url.Values{}
vals.Set("symbol", pair.String())
path := bitgetSpot + bitgetMarket + bitgetFundNetFlow
var resp []WhaleFundFlowResp
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

return nil, errSubaccountEmpty
}
path := bitgetUser + bitgetCreate + bitgetVirtualSubaccount
req := struct {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can replace this with maps and put it directly into the SendAuthenticatedHTTPRequest method call.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They were maps; you told me to replace them with structs back in May!

func (e *Exchange) GetConvertHistory(ctx context.Context, startTime, endTime time.Time, limit, pagination int64) (*ConvHistResp, error) {
var params Params
params.Values = make(url.Values)
err := params.prepareDateString(startTime, endTime, false, false)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Throughout this code base you can replace such occurances as follow

Suggested change
err := params.prepareDateString(startTime, endTime, false, false)
if err := params.prepareDateString(startTime, endTime, false, false); err != nil {

return nil, currency.ErrCurrencyPairEmpty
}
if p.Side == "" {
return nil, errSideEmpty
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replace this with order.ErrSideIsInvalid

return nil, errSideEmpty
}
if p.OrderType == "" {
return nil, errOrderTypeEmpty
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can be order.ErrUnsupportedOrderType or order.ErrTypeIsInvalid

return nil, errStrategyEmpty
}
if p.OrderType == "limit" && p.Price <= 0 {
return nil, errLimitPriceEmpty
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

replace with order.ErrPriceMustBeSetIfLimitOrder

func (e *Exchange) GetUnfilledOrders(ctx context.Context, pair currency.Pair, tpslType string, startTime, endTime time.Time, limit, pagination, orderID int64, acceptableDelay time.Duration) ([]UnfilledOrdersResp, error) {
var params Params
params.Values = make(url.Values)
err := params.prepareDateString(startTime, endTime, true, true)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shorten this too if err := ....

errSideEmpty = fmt.Errorf("%w: empty order side", order.ErrSideIsInvalid)
errOrderTypeEmpty = fmt.Errorf("%w: empty order type", order.ErrTypeIsInvalid)
errStrategyEmpty = errors.New("strategy cannot be empty")
errLimitPriceEmpty = fmt.Errorf("%w: Price below minimum for limit orders", limits.ErrPriceBelowMin)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

replace with order.ErrPriceMustBeSetIfLimitOrder

Copy link
Collaborator

@samuael samuael left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shalooom 🕊️ brother.
Just added another round of review on endpoint functions.

// QueryAnnouncements returns announcements from the exchange, filtered by type and time
func (e *Exchange) QueryAnnouncements(ctx context.Context, annType string, startTime, endTime time.Time, pagination uint64, limit uint8) ([]AnnouncementResp, error) {
var params Params
params.Values = make(url.Values)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move this to below the error check.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That can't be done; the error check calls a function on params which operates on params.Values

}
vals := url.Values{}
vals.Set("businessType", businessType)
var resp []AllTradeRatesResp
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can use []* for all response type declarations from now on; for consistency.
Instead of iterating over a slice and use the index to access elements, we can iterate over the slice but use the pointer and reduce the memory overhead.

Copy link
Collaborator Author

@cranktakular cranktakular Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In both cases you'd be putting the underlying data into memory at one point, being a slice of pointers just requires you to keep track of the pointer as well, which is more memory and more work for the GC!

// GetSpotTransactionRecords returns the user's spot transaction records
func (e *Exchange) GetSpotTransactionRecords(ctx context.Context, cur currency.Code, startTime, endTime time.Time, limit, pagination uint64) ([]SpotTrResp, error) {
var params Params
params.Values = make(url.Values)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you move this declaration below the error checks?
Apply to all occurences in this PR. always, params declaration after all the error checks pass.

// GetFuturesTransactionRecords returns the user's futures transaction records
func (e *Exchange) GetFuturesTransactionRecords(ctx context.Context, productType string, cur currency.Code, startTime, endTime time.Time, limit, pagination uint64) ([]FutureTrResp, error) {
var params Params
params.Values = make(url.Values)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

if pagination > 0 {
params.Values.Set("idLessThan", strconv.FormatUint(pagination, 10))
}
var resp []P2PTrResp
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
var resp []P2PTrResp
var resp []*P2PTrResp

}
vals := url.Values{}
vals.Set("symbol", pair.String())
vals.Set("period", period)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

period is not checked.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I only check for zero values in cases where zero values cause issues with the way the exchange responds.

}
vals := url.Values{}
vals.Set("symbol", pair.String())
vals.Set("period", period)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

period is not checked for zero value

if period != "" {
vals.Set("period", period)
}
vals.Set("coin", cur.String())
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cur is not checked if it is zero value.

return nil, errSubaccountEmpty
}
path := bitgetUser + bitgetCreate + bitgetVirtualSubaccount
req := struct {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
req := struct {
req := map[string][]string{"subAccountList": subaccounts}

vals := url.Values{}
vals.Set("symbol", pair.String())
vals.Set("precision", precision)
vals.Set("limit", limit)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are these variables not being checked for zero value?

Copy link
Collaborator

@shazbert shazbert left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will start to integrate this as priority and will help out with any changes

Unsubscriber: e.Unsubscribe,
GenerateSubscriptions: e.generateDefaultSubscriptions,
Features: &e.Features.Supports.WebsocketCapabilities,
MaxWebsocketSubscriptionsPerConnection: 240,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
MaxWebsocketSubscriptionsPerConnection: 240,
MaxWebsocketSubscriptionsPerConnection: 1000,

A single connection can subscribe up to 1000 Streams;
See: https://bitgetlimited.github.io/apidoc/en/spot/#overview

Also please use multi-connection management for future scaling options see: #2109
Will add a diff or push to branch if I can get around to it before you come back.

Copy link
Collaborator

@shazbert shazbert left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have updated accounts which needs more testing, migrated to multi-connection and added some fixes to subs. On login for the private conn it waits until it has a confirmation message.

Some things that need to be addressed:

  • futures asset needs to be broken up to USDT margined, USDC margined and Coin Margined futures.
  • margin and cross margin subs are semi broken
  • linter/misc/spell needs fixing
  • bitget config needs to be added to config_example.json

Will keep pushing things as I came across them. 🚀


// QueryAnnouncements returns announcements from the exchange, filtered by type and time
func (e *Exchange) QueryAnnouncements(ctx context.Context, annType string, startTime, endTime time.Time, pagination uint64, limit uint8) ([]AnnouncementResp, error) {
var params Params
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Params type can have url.Values unexported and then just have methods to the underlying which calls make when there is a nil reference, stops you having to do params.Values = make(url.Values)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I found a better way to do this.

Ryan O'Hara-Reid and others added 3 commits November 24, 2025 10:17
- Added websocket support for placing spot and futures orders with validation checks.
- Implemented cancellation of orders via websocket for both spot and futures.
- Created request and response types for websocket interactions.
- Added tests for websocket order submission and cancellation to ensure proper functionality.
- Updated order types to include settlement currency for derivatives.
shazbert pushed a commit to shazbert/gocryptotrader that referenced this pull request Dec 11, 2025
@github-actions
Copy link

This PR is stale because it has been open 21 days with no activity. Please provide an update on the progress of this PR.

@github-actions github-actions bot added the stale label Dec 19, 2025
@github-actions github-actions bot removed the stale label Jan 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review me This pull request is ready for review

Projects

Status: In review

Development

Successfully merging this pull request may close these issues.

7 participants