-
Notifications
You must be signed in to change notification settings - Fork 820
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
base: master
Are you sure you want to change the base?
Coinbase: Update exchange implementation #1480
Conversation
Continual enhance of Coinbase tests The revamp continues Oh jeez the Orderbook part's unfinished don't look Coinbase revamp, Orderbook still unfinished
…otrader into coinbase_api_revamp
V3 done, onto V2 Coinbase revamp nears completion Coinbase revamp nears completion Test commit should fail Coinbase revamp nears completion
There was a problem hiding this 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/coinbasepro/coinbasepro.go
Outdated
startDateString = "start_date" | ||
endDateString = "end_date" | ||
|
||
errIntervalNotSupported = "interval not supported" |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return an error then 🚀
exchanges/coinbasepro/coinbasepro.go
Outdated
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") |
There was a problem hiding this comment.
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.
There was a problem hiding this 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 🎉
There was a problem hiding this 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
There was a problem hiding this 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
There was a problem hiding this 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
There was a problem hiding this 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, |
There was a problem hiding this comment.
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
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) | ||
} | ||
} |
There was a problem hiding this comment.
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
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{ |
There was a problem hiding this comment.
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,
}
}
exchanges/protocol/features.go
Outdated
// FullPayloadSubscribe flushes and changes full subscription on websocket | ||
// connection by subscribing with full default stream channel list | ||
FullPayloadSubscribe bool `json:"fullPayloadSubscribe,omitempty"` |
There was a problem hiding this comment.
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
There was a problem hiding this 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.
exchanges/coinbasepro/coinbasepro.go
Outdated
startDateString = "start_date" | ||
endDateString = "end_date" | ||
|
||
errIntervalNotSupported = "interval not supported" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return an error then 🚀
exchanges/coinbasepro/coinbasepro.go
Outdated
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) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is not done
exchanges/coinbasepro/coinbasepro.go
Outdated
|
||
return fee | ||
// UnmarshalJSON unmarshals the JSON input into a UnixTimestamp type | ||
func (t *UnixTimestamp) UnmarshalJSON(b []byte) error { |
There was a problem hiding this comment.
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
exchanges/coinbasepro/coinbasepro.go
Outdated
} 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 { |
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
reinstate?
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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"` |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ValCur
-> ValueWithCurrency
timestamp, err := time.Parse(time.RFC3339, string(data)) | ||
if err != nil { | ||
return time.Time{}, err | ||
} | ||
return timestamp, nil |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
linter issue here
There was a problem hiding this 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 := ¤cy.PairFormat{Delimiter: currency.DashDelimiter, Uppercase: true} | ||
configFmt := ¤cy.PairFormat{Delimiter: currency.DashDelimiter, Uppercase: true} | ||
err := c.SetGlobalPairsManager(requestFmt, configFmt, asset.Spot) | ||
err := c.SetGlobalPairsManager(requestFmt, configFmt, asset.Spot, asset.Futures) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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) { |
There was a problem hiding this 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.
exchanges/coinbasepro/coinbasepro.go
Outdated
// 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is this for?
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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()) { |
There was a problem hiding this comment.
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.
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.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.
Checklist
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.