Skip to content

Conversation

cranktakular
Copy link
Collaborator

@cranktakular cranktakular commented Feb 14, 2024

PR Description

Updating Coinbase to the Advanced Trade and Sign In With Coinbase APIs, as well as a few tiny fixes of other parts of the codebase found along the way.

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)
  • 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

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

The only tests which seem to be failing (exchanges/kucoin, database/models/postgres, config, and common/file) are parts that I haven't substantively changed, and which seem to be failing on master too.

Continual enhance of Coinbase tests

The revamp continues

Oh jeez the Orderbook part's unfinished don't look

Coinbase revamp, Orderbook still unfinished
V3 done, onto V2

Coinbase revamp nears completion

Coinbase revamp nears completion

Test commit should fail

Coinbase revamp nears completion
@thrasher- thrasher- changed the title Coinbase api revamp Coinbase: Update exchange implementation Feb 14, 2024
@thrasher- thrasher- requested a review from a team February 14, 2024 23:40
Copy link
Collaborator

@gloriousCode gloriousCode left a comment

Choose a reason for hiding this comment

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

Thanks for updating against the API docs after last time
Auth and unauth works well, coverage is excellent. Just some really quick changes from me, but I am happy with its state

@@ -40,12 +40,12 @@ var (
ErrAmountMustBeSet = errors.New("amount must be set")
ErrClientOrderIDMustBeSet = errors.New("client order ID must be set")
ErrUnknownSubmissionAmountType = errors.New("unknown submission amount type")
ErrIDNotSet = errors.New("ID not set")
Copy link
Collaborator

@gloriousCode gloriousCode Aug 11, 2025

Choose a reason for hiding this comment

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

[Required] You have exported this but it is unused

sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders)
curSweeps, err := e.ListFuturesSweeps(t.Context())
assert.NoError(t, err)
partialSkip := false
Copy link
Collaborator

@gloriousCode gloriousCode Aug 11, 2025

Choose a reason for hiding this comment

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

[Optional] Remove this extra handholding please
Test works fine just as

_, err := e.CancelPendingFuturesSweep(t.Context())
	assert.NoError(t, err)

sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders)
curSweeps, err := e.ListFuturesSweeps(t.Context())
assert.NoError(t, err)
preCancel := false
Copy link
Collaborator

@gloriousCode gloriousCode Aug 11, 2025

Choose a reason for hiding this comment

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

[Optional] Remove this handholding stuff. Just call the func

ordID, err := e.ListOrders(t.Context(), nil, nil, nil, nil, nil, nil, "", "", "", "", "", "", 0, 10, time.Time{}, time.Time{}, currency.Code{})
assert.NoError(t, err)
if ordID == nil || len(ordID.Orders) == 0 {
t.Skip(skipInsufficientOrders)
Copy link
Collaborator

@gloriousCode gloriousCode Aug 11, 2025

Choose a reason for hiding this comment

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

[Optional] rm handholdy

	resp, err := e.GetOrderInfo(t.Context(), "123", testPairStable, asset.Spot)

is fine

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Looks like you meant to make this comment about the TestGetOrderInfo function that comes way later. I've removed the handholding there, but not here.

Copy link
Collaborator

@gbjk gbjk left a comment

Choose a reason for hiding this comment

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

Great work 👍

Partial review with feedback on first 28 files.

}

// createOrderConfig populates the OrderConfiguration struct
func createOrderConfig(orderType order.Type, timeInForce order.TimeInForce, stopDirection string, baseAmount, quoteAmount, limitPrice, stopPrice, bucketSize float64, endTime time.Time, postOnly, rfqDisabled bool, bucketNumber int64, bucketDuration time.Duration) (OrderConfiguration, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

🚧 Can we create a type for this, and then embed that type into the structs for PreviewOrderInfo and PlaceOrderInfo ?
Also, once we've done that, if there's a big overlap between those 2 types, can we just make an orderInfoReq that contains a union of the fields?

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 mean a type for the parameters?

I'm a bit concerned about the user experience behind combining those types due to the points where they don't overlap; we'd at least need some communication over which fields are only relevant to one endpoint.

Copy link
Collaborator

Choose a reason for hiding this comment

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

A type for the fields that both of them send to createOrderConfig, yes, embedded in both.

However you don't need to create a orderInfoReq type to union if it doesn't feel right to you 🙂

Copy link
Collaborator

@gbjk gbjk left a comment

Choose a reason for hiding this comment

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

Good work, thanks for those changes.

Another partial review, up to 35 files.

Comment on lines 122 to 151
var sliToSend []ticker.Price
aliases := e.pairAliases.GetAliases()
for i := range wsTicker {
for j := range wsTicker[i].Tickers {
tickAlias := aliases[wsTicker[i].Tickers[j].ProductID]
newTick := ticker.Price{
LastUpdated: inc.Timestamp,
AssetType: asset.Spot,
ExchangeName: e.Name,
High: wsTicker[i].Tickers[j].High24H.Float64(),
Low: wsTicker[i].Tickers[j].Low24H.Float64(),
Last: wsTicker[i].Tickers[j].Price.Float64(),
Volume: wsTicker[i].Tickers[j].Volume24H.Float64(),
Bid: wsTicker[i].Tickers[j].BestBid.Float64(),
BidSize: wsTicker[i].Tickers[j].BestBidQuantity.Float64(),
Ask: wsTicker[i].Tickers[j].BestAsk.Float64(),
AskSize: wsTicker[i].Tickers[j].BestAskQuantity.Float64(),
}
var errs error
for k := range tickAlias {
isEnabled, err := e.CurrencyPairs.IsPairEnabled(tickAlias[k], asset.Spot)
if err != nil {
errs = common.AppendError(errs, err)
continue
}
if isEnabled {
newTick.Pair = tickAlias[k]
sliToSend = append(sliToSend, newTick)
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

🚧
Can this go in a function please?
Same for the other sections below.
wsHandleData is already unwieldy enough as it is, but inlining all this processing just makes it huge.
Also implies more targeted unit tests, probably.

Copy link
Collaborator Author

@cranktakular cranktakular Aug 19, 2025

Choose a reason for hiding this comment

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

I've split these out, but have left the tests together for now; the test is already quite thorough, and splitting that out seems like quite a bit more of a hassle.

Copy link
Collaborator

@gbjk gbjk left a comment

Choose a reason for hiding this comment

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

Changes since last review ... reviewed.

Great work.

Just one thing.

@@ -365,29 +365,34 @@ type ErrorResponse struct {
NewOrderFailureReason string `json:"new_order_failure_reason"`
}

// SharedOrderConfig contains order configuration information used in both PlaceOrderInfo and PreviewOrderInfo
type SharedOrderConfig struct {
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 we need to say it's shared. It's just OrderConfig.
I think you're only thinking about it being shared because of where you started.

Suggested change
type SharedOrderConfig struct {
type OrderConfig struct {

Do not need to action.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

There's already an OrderConfiguration type, which contains the full, properly-formatted depth of information. Typing that out, "shared" isn't the right way to distinguish this, but the other ideas I have (OrderConfigPreparation, OrderConfigConstructor) seem more verbose and also a bit wrong.

I do think that just "OrderConfig" is a bit close for comfort.

Copy link
Collaborator

Choose a reason for hiding this comment

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

You're right, that would not fly.

  • OrderConfiguration smells like OrderType or OrderConds
    Everything in there is about order type conditions.

  • SharedOrderConfig lives in 2 structs named *OrderInfo. So I think it's actually just OrderInfo 😄

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The OrderConfiguration naming comes from what the exchange itself calls it. Would like to double-check changing it.

But I'll apply that change from SharedOrderConfig > OrderInfo.

Copy link
Collaborator

@gbjk gbjk left a comment

Choose a reason for hiding this comment

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

56 / 57 files reviewed

Only coinbasepro_wrapper.go remaining.

@@ -3,6 +3,7 @@ package protocol
// Features holds all variables for the exchanges supported features
// for a protocol (e.g REST or Websocket)
type Features struct {
// TickerBatching allows the REST endpoint to fetch the entire ticker list available to the exchange
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 say REST
  • Are we really saying it's always the entire thing. That feels like an exchange implementation thing. I think all we're saying here is "the exchange protocol allows batching". I could be wrong.
  • Typo at the end of the sentence, but we can omit it.
    Guessing we want:
Suggested change
// TickerBatching allows the REST endpoint to fetch the entire ticker list available to the exchange
// TickerBatching allows api to fetch the tickers in batches

Though ... I mean ... it's not adding any information for me 🤷

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The state of this value doesn't seem to be directly used by our engine anywhere, so we don't functionally enshrine a definition that way.

It's hard to tell exactly why I added that comment, as it was written 20 months ago, but I suspect that I didn't understand exactly what that meant, asked shazbert/thrasher, and they responded with that. But really, even now, I wouldn't consider myself to have an authoritative view on what the definition ought to be. Maybe wait for input from others?

Copy link
Collaborator

Choose a reason for hiding this comment

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

"the exchange protocol allows batching": this is correct. As in, the exchange supports an API which allows us to do a batch request for all available tickers.

Engine's syncer will attempt to use REST ticker batching if possible since it can update all tickers with a single API request as opposed to iterating over a pairs list and fetching a ticker with numerous requests:

https://github.com/thrasher-corp/gocryptotrader/blob/master/engine/sync_manager.go#L572

Which will then check if b.Features.Supports.RESTCapabilities.TickerBatching == true

This is mainly a feature supported via REST which is why there's an emphasis on it, since websocket will just stream events in realtime or close to it.

As for the comment, both websocket and REST have their own supported Features and I don't think we need a comment for it

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 there's a possible confusion; "allows batching" as in "you request a batch of tickers for a pair, and can receive up to 50 ticks for that pair", or as in "you can simultaneously request the latest ticker for every pair on the exchange", or even "you can simultaneously request the latest ticker for up to N pairs on the exchange".

I expect that's what I was initially confused about.

Copy link
Collaborator

Choose a reason for hiding this comment

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

In all cases it's "you can request the latest tickers for every pair on the exchange in a single batched request". Can't think of any exchanges which require pagination for doing a bulk request of tickers off the top of my head

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ah, I seem to have confused it with candles, my bad.

Yeah it should be fine without a comment then.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Note: Still outstanding to remove the comment.

@@ -370,6 +379,7 @@ const (
Chase // chase limit order
OptimalLimit
MarketMakerProtection
Bracket // Sets both a profit target and a stop loss simultaneously.
Copy link
Collaborator

Choose a reason for hiding this comment

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

🚧 We already have StopLimit and StopMarket types, which is a synonym for Bracket order.
So I think that this is about treating the text representation of StopLimit as BRACKET for Coinbase, maybe?

Suggested change
Bracket // Sets both a profit target and a stop loss simultaneously.
Bracket // Sets both a profit target and a stop loss simultaneously.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Are those a synonym for Bracket? I thought that Bracket would be Stop | TakeProfit (and now that such hybrid order types are allowed, I could change it to that).

Copy link
Collaborator

Choose a reason for hiding this comment

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

You're right, I wasn't focusing on the difference between TakeProfit and Limit.

I think you're right that we should switch to a hybrid type, but I think that needs more input from others.

@thrasher- thrasher- requested review from samuael and thrasher- August 19, 2025 06:06
@cranktakular cranktakular requested a review from gbjk August 19, 2025 06:13
Copy link
Collaborator

@gloriousCode gloriousCode left a comment

Choose a reason for hiding this comment

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

I think this is ready to merge. Tested auth and unauth and ensured the websocket order book stays in sync

Nice work Mr. Crank

}

// ExchangeDepositAddresses is a map of currencies to their deposit addresses
type ExchangeDepositAddresses map[string][]deposit.Address
Copy link
Collaborator

Choose a reason for hiding this comment

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

A surprising change in the Coinbase PR 😄

Comment on lines 158 to 159
closedStatuses = []string{"FILLED", "CANCELLED", "EXPIRED", "FAILED"}
openStatus = []string{"OPEN"}
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 really think these are needed, but its not worth a change unless other things come up

Comment on lines 228 to 231
"trade_incentive_metadata": map[string]any{
"user_incentive_id": userIncentiveID,
"code_val": codeVal,
},
Copy link
Collaborator

Choose a reason for hiding this comment

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

no real need for map[string]any when you only use map[string]string. Not an issue, just an observation

if productID == "" {
return nil, errProductIDEmpty
}
if !common.StringSliceContains(allowedGranularities, granularity) {
gran, ok := allowedGranularities[granularity]
Copy link
Collaborator

Choose a reason for hiding this comment

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

My gran is always allowed to retrieve historical Klines, thank you

Copy link
Collaborator

@gbjk gbjk left a comment

Choose a reason for hiding this comment

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

Thanks - Minor changes

@@ -365,29 +365,34 @@ type ErrorResponse struct {
NewOrderFailureReason string `json:"new_order_failure_reason"`
}

// SharedOrderConfig contains order configuration information used in both PlaceOrderInfo and PreviewOrderInfo
type SharedOrderConfig 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're right, that would not fly.

  • OrderConfiguration smells like OrderType or OrderConds
    Everything in there is about order type conditions.

  • SharedOrderConfig lives in 2 structs named *OrderInfo. So I think it's actually just OrderInfo 😄

err := json.Unmarshal(respRaw, &msgType)
if err != nil {
// tickerDataHandler handles ticker data from the websocket
func (e *Exchange) tickerDataHandler(resp *StandardWebsocketResponse) error {
Copy link
Collaborator

Choose a reason for hiding this comment

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

🚧 Thanks for breaking these out.
Can we normalise naming to match other exchanges, which call these wsProcess* ?
That is: I don't (ever) think "Data" adds anything, and we lack ws context.

Suggested change
func (e *Exchange) tickerDataHandler(resp *StandardWebsocketResponse) error {
func (e *Exchange) wsProcessTicker(resp StandardWebsocketResponse) error {

Can probably hit them all with a regex:

:%s/\s(.*?)DataHandler\(/wsProcess\1/g

@@ -3,6 +3,7 @@ package protocol
// Features holds all variables for the exchanges supported features
// for a protocol (e.g REST or Websocket)
type Features struct {
// TickerBatching allows the REST endpoint to fetch the entire ticker list available to the exchange
Copy link
Collaborator

Choose a reason for hiding this comment

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

Note: Still outstanding to remove the comment.

@@ -370,6 +379,7 @@ const (
Chase // chase limit order
OptimalLimit
MarketMakerProtection
Bracket // Sets both a profit target and a stop loss simultaneously.
Copy link
Collaborator

Choose a reason for hiding this comment

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

You're right, I wasn't focusing on the difference between TakeProfit and Limit.

I think you're right that we should switch to a hybrid type, but I think that needs more input from others.

Comment on lines 423 to 437
req := map[string]any{
"client_order_id": ord.ClientOID,
"product_id": ord.ProductID,
"side": ord.Side,
"order_configuration": orderConfig,
"retail_portfolio_id": ord.RetailPortfolioID,
"preview_id": ord.PreviewID,
"attached_order_configuration": ord.AttachedOrderConfiguration,
}
if ord.MarginType != "" {
req["margin_type"] = FormatMarginType(ord.MarginType)
}
if ord.Leverage != 0 && ord.Leverage != 1 {
req["leverage"] = strconv.FormatFloat(ord.Leverage, 'f', -1, 64)
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

🚧 I realised that this appears to be a misplaced adhoc map instead of a PlaceOrderInfo having json tags and marhsaling itself correctly.

Sorry if I'm missing something, but everything here appears to suggest that the entire thing should be a normal request struct.

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? I'd need to rewrite the authenticated request function to be able to place bespoke structs rather than map[string]any objects in for the body of a request. And applying this broadly would require rewriting parts of 20 function calls.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yep, I think you've understood it already, because "I'd need to X" matches exactly what you need to do.
We use structured data everywhere else.
If it's possible, we should do it.

Would you like me to provide a patch?

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 would be quite helpful.

Copy link
Collaborator

@gbjk gbjk left a comment

Choose a reason for hiding this comment

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

Sorry. I know you'll hate me for this, but now is the right time to do this:
Can we rename coinbasepro to coinbase inline with their renaming 3 years ago.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
high priority review me This pull request is ready for review
Projects
Status: In review
Development

Successfully merging this pull request may close these issues.

5 participants