Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Coinbase: Update exchange implementation #1480

Open
wants to merge 104 commits into
base: master
Choose a base branch
from

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

@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.

Thanks heaps for this update, this is looking real good. I have tested the public endpoints, next will be private then websocket testing.

exchanges/asset/asset_test.go Show resolved Hide resolved
startDateString = "start_date"
endDateString = "end_date"

errIntervalNotSupported = "interval not supported"
Copy link
Collaborator

Choose a reason for hiding this comment

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

this can be an error with errors.New() or use kline package level error.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It can't, since it's used in a function that returns a string, not an error.

Copy link
Collaborator

Choose a reason for hiding this comment

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

return an error then 🚀

engine/currency_state_manager.md Outdated Show resolved Hide resolved
errInvalidGranularity = errors.New("invalid granularity")
errOrderFailedToCancel = errors.New("failed to cancel order")
errUnrecognisedStatusType = errors.New("unrecognised status type")
errPairEmpty = errors.New("pair 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.

This can be a currency package error currency.ErrCurrencyPairEmpty also has error for empty currency Code, there are others for unknown side/type errors in the order package. If you can, just check across these and update to use those package errors please.

exchanges/coinbasepro/coinbasepro.go Outdated Show resolved Hide resolved
exchanges/coinbasepro/coinbasepro_wrapper.go Outdated Show resolved Hide resolved
exchanges/coinbasepro/coinbasepro_wrapper.go Outdated Show resolved Hide resolved
exchanges/coinbasepro/coinbasepro_wrapper.go Outdated Show resolved Hide resolved
exchanges/exchange.go Outdated Show resolved Hide resolved
exchanges/stream/buffer/buffer.go Outdated Show resolved Hide resolved
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.

Nice work cranktakular. Please rebase/merge master. I am happy with the work you have done 🎉

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.

Happy with your changes, thanks! There's an intermitted exchange_wrapper_standards tests breaking now post-master-merge. I think I'm good once that's sorted

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.

Just have a question on a comment. Then after that if you could rebase/merge I can approve

exchanges/subscription/template_test.go Outdated Show resolved Hide resolved
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.

Unfortunately it has been difficult to test the scenario due to CBP websocket not respecting config subscriptions:

 "subscriptions": [
     {
      "enabled": false,
      "channel": "heartbeat"
     },
     {
      "enabled": false,
      "channel": "status"
     },
     {
      "enabled": false,
      "channel": "ticker",
      "asset": "spot"
     },
     {
      "enabled": false,
      "channel": "candles",
      "asset": "spot"
     },
     {
      "enabled": false,
      "channel": "allTrades",
      "asset": "spot"
     },
     {
      "enabled": false,
      "channel": "orderbook",
      "asset": "spot"
     },
     {
      "enabled": true,
      "channel": "account",
      "authenticated": true
     },
     {
      "enabled": false,
      "channel": "ticker_batch",
      "asset": "spot"
     }
[DEBUG] | WEBSOCKET | 30/10/2024 10:51:41 | CoinbasePro wss://advanced-trade-ws.coinbase.com: Sending message: {"type":"subscribe","product_ids":["BTC-USD"],"channel":"candles","timestamp":"1730245901"}
[DEBUG] | WEBSOCKET | 30/10/2024 10:51:41 | CoinbasePro wss://advanced-trade-ws.coinbase.com: Sending message: {"type":"subscribe","channel":"heartbeats","timestamp":"1730245901"}
[DEBUG] | WEBSOCKET | 30/10/2024 10:51:41 | CoinbasePro wss://advanced-trade-ws.coinbase.com: Sending message: {"type":"subscribe","channel":"user","signature":"POP","api_key":"CAT","timestamp":"1730245901"}
[DEBUG] | WEBSOCKET | 30/10/2024 10:51:41 | CoinbasePro wss://advanced-trade-ws.coinbase.com: Sending message: {"type":"subscribe","product_ids":["BTC-USD"],"channel":"market_trades","timestamp":"1730245901"}
[DEBUG] | WEBSOCKET | 30/10/2024 10:51:41 | CoinbasePro wss://advanced-trade-ws.coinbase.com: Sending message: {"type":"subscribe","product_ids":["BTC-USD"],"channel":"level2","timestamp":"1730245901"}
[DEBUG] | WEBSOCKET | 30/10/2024 10:51:41 | CoinbasePro wss://advanced-trade-ws.coinbase.com: Sending message: {"type":"subscribe","product_ids":["BTC-USD"],"channel":"ticker","timestamp":"1730245901"}

This will need to be fixed

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.

Not many requests other than the reversion I already commented on. Good stuff

Type: oType,
Side: oSide,
Status: oStatus,
AssetType: asset.Spot,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Given the usage of asset.All for "user" and that this can return productType of either SPOT or FUTURE I think you should be returning the appropriate asset type

Comment on lines 494 to 509
func statusToStandardStatus(stat string) (order.Status, error) {
switch stat {
case "received":
return order.New, nil
case "open":
return order.Active, nil
case "done":
return order.Filled, nil
case "match":
return order.PartiallyFilled, nil
case "change", "activate":
return order.Active, nil
default:
return order.UnknownStatus, fmt.Errorf("%w %v", errUnrecognisedStatusType, stat)
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

These may have changed since you did this:
https://docs.cdp.coinbase.com/advanced-trade/docs/ws-channels#user-channel
I note that CB seems to be doing things in upper case and I'm not seeing case handling anywhere

Comment on lines 262 to 275
if err != nil {
return warnString, err
}
var oSide order.Side
oSide, err = order.StringToOrderSide(wsUser[i].Orders[j].OrderSide)
if err != nil {
return warnString, err
}
var oStatus order.Status
oStatus, err = statusToStandardStatus(wsUser[i].Orders[j].Status)
if err != nil {
return warnString, err
}
sliToSend = append(sliToSend, order.Detail{
Copy link
Collaborator

Choose a reason for hiding this comment

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

Use the following just for these instances of if err != nil

if err != nil {
	c.Websocket.DataHandler <- order.ClassificationError{
				Exchange: c.Name,
				Err:      err,
	}
}

Comment on lines 38 to 40
// FullPayloadSubscribe flushes and changes full subscription on websocket
// connection by subscribing with full default stream channel list
FullPayloadSubscribe bool `json:"fullPayloadSubscribe,omitempty"`
Copy link
Collaborator

Choose a reason for hiding this comment

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

This appears unused and the only addition

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.

Good stuff minor requests.

startDateString = "start_date"
endDateString = "end_date"

errIntervalNotSupported = "interval not supported"
Copy link
Collaborator

Choose a reason for hiding this comment

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

return an error then 🚀

if err := c.SendHTTPRequest(ctx, exchange.RestSpot, path, &orderbook); err != nil {
return nil, err
// GetProductBookV3 returns a list of bids/asks for a single product
func (c *CoinbasePro) GetProductBookV3(ctx context.Context, productID string, limit uint16, authenticated bool) (*ProductBook, error) {
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 not done


return fee
// UnmarshalJSON unmarshals the JSON input into a UnixTimestamp type
func (t *UnixTimestamp) UnmarshalJSON(b []byte) error {
Copy link
Collaborator

Choose a reason for hiding this comment

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

remove UnixTimestamp type and use types.Time

} else if c.Equal(currency.EUR) {
fee = 0.15
// UnmarshalJSON unmarshals the JSON input into a UnixTimestamp type
func (t *UnixTimestampMilli) UnmarshalJSON(b []byte) error {
Copy link
Collaborator

Choose a reason for hiding this comment

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

remove UnixTimestampMilli and use types.Time

}

// TestWsAuth dials websocket, sends login request.
func TestWsAuth(t *testing.T) {
if !c.Websocket.IsEnabled() && !c.API.AuthenticatedWebsocketSupport || !sharedtestvalues.AreAPICredentialsSet(c) {
// t.Parallel()
Copy link
Collaborator

Choose a reason for hiding this comment

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

reinstate?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Just gonna rm that, since other tests interfere with it.


// AmCur is a sub-struct used in ListNotificationsSubData, WalletData, TransactionData,
// DeposWithdrData, and PaymentMethodData
type AmCur struct {
Copy link
Collaborator

Choose a reason for hiding this comment

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

AmCur -> AmountWithCurrency

// OrderBookResp holds information on bids and asks for a particular currency pair, used for unmarshalling in
// GetProductBookV1
type OrderBookResp struct {
Bids [][3]interface{} `json:"bids"`
Copy link
Collaborator

Choose a reason for hiding this comment

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

please convert interface{} -> any across this PR.

// ValCur is a sub-struct used in the types Account, NativeAndRaw, DetailedPortfolioResponse,
// FuturesBalanceSummary, ListFuturesSweepsResponse, PerpetualsPortfolioSummary, PerpPositionDetail,
// FeeStruct, AmScale, and ConvertResponse
type ValCur struct {
Copy link
Collaborator

Choose a reason for hiding this comment

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

ValCur -> ValueWithCurrency

Comment on lines 538 to 542
timestamp, err := time.Parse(time.RFC3339, string(data))
if err != nil {
return time.Time{}, err
}
return timestamp, nil
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
timestamp, err := time.Parse(time.RFC3339, string(data))
if err != nil {
return time.Time{}, err
}
return timestamp, nil
return time.Parse(time.RFC3339, string(data))

}

// strategyDecoder is a helper function that converts a Coinbase Pro time in force string to a few standardised bools
func strategyDecoder(str string) (bool, bool, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

linter issue here

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.

You are going to have to swap over to using JWT authentication. I cannot test the old key format.

requestFmt := &currency.PairFormat{Delimiter: currency.DashDelimiter, Uppercase: true}
configFmt := &currency.PairFormat{Delimiter: currency.DashDelimiter, Uppercase: true}
err := c.SetGlobalPairsManager(requestFmt, configFmt, asset.Spot)
err := c.SetGlobalPairsManager(requestFmt, configFmt, asset.Spot, asset.Futures)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please add:

diff --git a/exchanges/coinbasepro/coinbasepro_wrapper.go b/exchanges/coinbasepro/coinbasepro_wrapper.go
index a675e7ba9..8ca87ef5a 100644
--- a/exchanges/coinbasepro/coinbasepro_wrapper.go
+++ b/exchanges/coinbasepro/coinbasepro_wrapper.go
@@ -44,6 +44,10 @@ func (c *CoinbasePro) SetDefaults() {
        if err != nil {
                log.Errorln(log.ExchangeSys, err)
        }
+       err = c.DisableAssetWebsocketSupport(asset.Futures)
+       if err != nil {
+               log.Errorln(log.ExchangeSys, err)
+       }
        c.Features = exchange.Features{
                Supports: exchange.FeaturesSupported{
                        REST:      true,
}

As there are currently no websocket market data support for futures.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

You want that done, even though there is futures websocket support, limited to the user's orders?

sort.Sort(trade.ByDate(resp))
return resp, nil
// GetRecentTrades returns the most recent trades for a currency and asset
func (c *CoinbasePro) GetRecentTrades(_ context.Context, _ currency.Pair, _ asset.Item) ([]trade.Data, error) {
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
func (c *CoinbasePro) GetRecentTrades(_ context.Context, _ currency.Pair, _ asset.Item) ([]trade.Data, error) {
func (c *CoinbasePro) GetRecentTrades(context.Context, currency.Pair, asset.Item) ([]trade.Data, error) {

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.

Quick review on new changes. Then I can do a thorough review before next week.

Comment on lines 1524 to 1547
// The code below mostly works, but seems to lead to bad results on the signature step. Deferring until later
// head := map[string]any{"kid": creds.Key, "typ": "JWT", "alg": "ES256", "nonce": nonce}
// headJSON, err := json.Marshal(head)
// if err != nil {
// return "", time.Time{}, err
// }
// headEncode := base64URLEncode(headJSON)
// regTime := time.Now()
// body := map[string]any{"iss": "cdp", "nbf": regTime.Unix(), "exp": regTime.Add(time.Minute * 2).Unix(), "sub": creds.Key /*, "aud": "retail_rest_api_proxy"*/}
// if uri != "" {
// body["uri"] = uri
// }
// bodyJSON, err := json.Marshal(body)
// if err != nil {
// return "", time.Time{}, err
// }
// bodyEncode := base64URLEncode(bodyJSON)
// hash := sha256.Sum256([]byte(headEncode + "." + bodyEncode))
// sig, err := ecdsa.SignASN1(rand.Reader, key, hash[:])
// if err != nil {
// return "", time.Time{}, err
// }
// sigEncode := base64URLEncode(sig)
// return headEncode + "." + bodyEncode + "." + sigEncode, regTime, nil
Copy link
Collaborator

Choose a reason for hiding this comment

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

What is this for?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

As comment at the top of the block says, it's my implementation of the JWT protocol, so we won't have to import a separate package to handle that. Alas, there's some issue I can't identify with generating the signature, which has it create an invalid one, so this can't be used right now.

return headEncode + "." + bodyEncode + "." + sigEncode, nil
// GetWSJWT returns a JWT, using a stored one of it's provided, and generating a new one otherwise
func (c *CoinbasePro) GetWSJWT() (string, error) {
c.mut.RLock()
Copy link
Collaborator

Choose a reason for hiding this comment

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

suggestion: You can simplify this with a normal mutex and just defer the unlock. Then you don't need to worry about most of whats going on below. We can optimise later if this turns into a bottleneck which it most likely won't be.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Is there a point in doing that now, considering how I've already written this handling?

If so, let me know, and I'll refactor it.

// GetWSJWT returns a JWT, using a stored one of it's provided, and generating a new one otherwise
func (c *CoinbasePro) GetWSJWT() (string, error) {
c.mut.RLock()
if c.jwtLastRegen.Add(time.Minute * 2).After(time.Now()) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

instead of JWTLasTRegen just assign an expiry value so the addition op doesn't need to happen.

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 test(s) fix This is to denote the PR is fixing a build test issue
Projects
Status: In review
Development

Successfully merging this pull request may close these issues.

5 participants