mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-13 23:16:45 +00:00
Coinbase: Update exchange implementation (#1480)
* Slight enhance of Coinbase tests Continual enhance of Coinbase tests The revamp continues Oh jeez the Orderbook part's unfinished don't look Coinbase revamp, Orderbook still unfinished * Coinbase revamp; CreateReport is still WIP * More coinbase improvements; onto sandbox testing * Coinbase revamp continues * Coinbase revamp continues * Coinbasepro revamp is ceaseless * Coinbase revamp, starting on advanced trade API * Coinbase Advanced Trade Starts in Ernest V3 done, onto V2 Coinbase revamp nears completion Coinbase revamp nears completion Test commit should fail Coinbase revamp nears completion * Coinbase revamp stage wrapper * Coinbase wrapper coherence continues * Coinbase wrapper continues writhing * Coinbase wrapper & codebase cleanup * Coinbase updates & wrap progress * More Coinbase wrapper progress * Wrapper is wrapped, kinda * Test & type checking * Coinbase REST revamp finished * Post-merge fix * WS revamp begins * WS Main Revamp Done? * CB websocket tidying up * Coinbase WS wrapperupperer * Coinbase revamp done?? * Linter progress * Continued lint cleanup * Further lint cleanup * Increased lint coverage * Does this fix all sloppy reassigns & shadowing? * Undoing retry policy change * Documentation regeneration * Coinbase code improvements * Providing warning about known issue * Updating an error to new format * Making gocritic happy * Review adherence * Endpoints moved to V3 & nil pointer fixes * Removing seemingly superfluous constant * Glorious improvements * Removing unused error * Partial public endpoint addition * Slight improvements * Wrapper improvements; still a few errors left in other packages * A lil Coinbase progress * Json cleaning * Lint appeasement * Config repair * Config fix (real) * Little fix * New public endpoint incorporation * Additional fixes * Improvements & Appeasements * LineSaver * Additional fixes * Another fix * Fixing picked nits * Quick fixies * Lil fixes * Subscriptions: Add List.Enabled * CoinbasePro: Add subscription templating * fixup! CoinbasePro: Add subscription templating * fixup! CoinbasePro: Add subscription templating * Comment fix * Subsequent fixes * Issues hopefully fixed * Lint fix * Glorious fixes * Json formatting * ShazNits * (L/N)i(n/)t * Adding a test * Tiny test improvement * Template patch testing * Fixes * Further shaznits * Lint nit * JWT move and other fixes * Small nits * Shaznit, singular * Post-merge fix * Post-merge fixes * Typo fix * Some glorious nits * Required changes * Stop going * Alias attempt * Alias fix & test cleanup * Test fix * GetDepositAddress logic improvement * Status update: Fixed * Lint fix * Happy birthday to PR 1480 * Cleanups * Necessary nit corrections * Fixing sillybug * As per request * Programming progress * Order fixes * Further fixies * Test fix * Pre-merge fixes * More shaznits * Context * Sonic error handling * Import fix * Better Sonic error handling * Perfect Sonic error handling? * F purge * Coinbase improvements * API Update Conformity * Coinbase continuation * Coinbase order improvements * Coinbase order improvements * CreateOrderConfig improvements * Managing API updates * Coinbase API update progression * jwt rename * Comment link fix * Coinbase v2 cleanup * Post-merge fixes * Review fixes * GK's suggestions * Linter fix * Minor gbjk fixes * Nit fixes * Merge fix * Lint fixes * Coinbase rename stage 1 * Coinbase rename stage 2 * Coinbase rename stage 3 * Coinbase rename stage 4 * Coinbase rename final fix * Coinbase: PoC on converting to request structs * Applying requested changes * Many review fixes, handled * Thrashed by nits * More minor modifications * The last nit!? --------- Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>
This commit is contained in:
127
exchanges/coinbase/README.md
Normal file
127
exchanges/coinbase/README.md
Normal file
@@ -0,0 +1,127 @@
|
||||
# GoCryptoTrader package Coinbase
|
||||
|
||||
<img src="/common/gctlogo.png?raw=true" width="350px" height="350px" hspace="70">
|
||||
|
||||
|
||||
[](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml)
|
||||
[](https://github.com/thrasher-corp/gocryptotrader/blob/master/LICENSE)
|
||||
[](https://godoc.org/github.com/thrasher-corp/gocryptotrader/exchanges/Coinbase)
|
||||
[](https://codecov.io/gh/thrasher-corp/gocryptotrader)
|
||||
[](https://goreportcard.com/report/github.com/thrasher-corp/gocryptotrader)
|
||||
|
||||
|
||||
This coinbase package is part of the GoCryptoTrader codebase.
|
||||
|
||||
## This is still in active development
|
||||
|
||||
You can track ideas, planned features and what's in progress on our [GoCryptoTrader Kanban board](https://github.com/orgs/thrasher-corp/projects/3).
|
||||
|
||||
Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader Slack](https://join.slack.com/t/gocryptotrader/shared_invite/zt-38z8abs3l-gH8AAOk8XND6DP5NfCiG_g)
|
||||
|
||||
## Coinbase Exchange
|
||||
|
||||
### Current Features
|
||||
|
||||
+ REST Support
|
||||
+ Websocket Support
|
||||
|
||||
### How to enable
|
||||
|
||||
+ [Enable via configuration](https://github.com/thrasher-corp/gocryptotrader/tree/master/config#enable-exchange-via-config-example)
|
||||
|
||||
+ Individual package example below:
|
||||
|
||||
```go
|
||||
// Exchanges will be abstracted out in further updates and examples will be
|
||||
// supplied then
|
||||
```
|
||||
|
||||
### How to do REST public/private calls
|
||||
|
||||
+ If enabled via "configuration".json file the exchange will be added to the
|
||||
IBotExchange array in the ```go var bot Bot``` and you will only be able to use
|
||||
the wrapper interface functions for accessing exchange data. View routines.go
|
||||
for an example of integration usage with GoCryptoTrader. Rudimentary example
|
||||
below:
|
||||
|
||||
main.go
|
||||
```go
|
||||
var c exchange.IBotExchange
|
||||
|
||||
for i := range bot.Exchanges {
|
||||
if bot.Exchanges[i].GetName() == "Coinbase" {
|
||||
c = bot.Exchanges[i]
|
||||
}
|
||||
}
|
||||
|
||||
// Public calls - wrapper functions
|
||||
|
||||
// Fetches current ticker information
|
||||
tick, err := c.UpdateTicker(...)
|
||||
if err != nil {
|
||||
// Handle error
|
||||
}
|
||||
|
||||
// Fetches current orderbook information
|
||||
ob, err := c.UpdateOrderbook(...)
|
||||
if err != nil {
|
||||
// Handle error
|
||||
}
|
||||
|
||||
// Private calls - wrapper functions - make sure your APIKEY and APISECRET are
|
||||
// set and AuthenticatedAPISupport is set to true
|
||||
|
||||
// Fetches current account information
|
||||
accountInfo, err := c.GetAccountInfo()
|
||||
if err != nil {
|
||||
// Handle error
|
||||
}
|
||||
```
|
||||
|
||||
+ If enabled via individually importing package, rudimentary example below:
|
||||
|
||||
```go
|
||||
// Public calls
|
||||
|
||||
// Fetches current ticker information
|
||||
ticker, err := c.GetTicker()
|
||||
if err != nil {
|
||||
// Handle error
|
||||
}
|
||||
|
||||
// Fetches current orderbook information
|
||||
ob, err := c.GetOrderBook()
|
||||
if err != nil {
|
||||
// Handle error
|
||||
}
|
||||
|
||||
// Private calls - make sure your APIKEY and APISECRET are set and
|
||||
// AuthenticatedAPISupport is set to true
|
||||
|
||||
// GetUserInfo returns account info
|
||||
accountInfo, err := c.GetUserInfo(...)
|
||||
if err != nil {
|
||||
// Handle error
|
||||
}
|
||||
|
||||
// Submits an order and the exchange and returns its tradeID
|
||||
tradeID, err := c.Trade(...)
|
||||
if err != nil {
|
||||
// Handle error
|
||||
}
|
||||
```
|
||||
|
||||
### How to do Websocket public/private calls
|
||||
|
||||
```go
|
||||
// Exchanges will be abstracted out in further updates and examples will be
|
||||
// supplied then
|
||||
```
|
||||
|
||||
## Donations
|
||||
|
||||
<img src="https://github.com/thrasher-corp/gocryptotrader/blob/master/web/src/assets/donate.png?raw=true" hspace="70">
|
||||
|
||||
If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to:
|
||||
|
||||
***bc1qk0jareu4jytc0cfrhr5wgshsq8282awpavfahc***
|
||||
1735
exchanges/coinbase/coinbase.go
Normal file
1735
exchanges/coinbase/coinbase.go
Normal file
File diff suppressed because it is too large
Load Diff
2047
exchanges/coinbase/coinbase_test.go
Normal file
2047
exchanges/coinbase/coinbase_test.go
Normal file
File diff suppressed because it is too large
Load Diff
2722
exchanges/coinbase/coinbase_types.go
Normal file
2722
exchanges/coinbase/coinbase_types.go
Normal file
File diff suppressed because it is too large
Load Diff
606
exchanges/coinbase/coinbase_websocket.go
Normal file
606
exchanges/coinbase/coinbase_websocket.go
Normal file
@@ -0,0 +1,606 @@
|
||||
package coinbase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strconv"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
gws "github.com/gorilla/websocket"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/thrasher-corp/gocryptotrader/common"
|
||||
"github.com/thrasher-corp/gocryptotrader/encoding/json"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchange/websocket"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/margin"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/subscription"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/trade"
|
||||
)
|
||||
|
||||
const (
|
||||
coinbaseWebsocketURL = "wss://advanced-trade-ws.coinbase.com"
|
||||
)
|
||||
|
||||
var subscriptionNames = map[string]string{
|
||||
subscription.HeartbeatChannel: "heartbeats",
|
||||
subscription.TickerChannel: "ticker",
|
||||
subscription.CandlesChannel: "candles",
|
||||
subscription.AllTradesChannel: "market_trades",
|
||||
subscription.OrderbookChannel: "level2",
|
||||
subscription.MyAccountChannel: "user",
|
||||
"status": "status",
|
||||
"ticker_batch": "ticker_batch",
|
||||
/* Not Implemented:
|
||||
"futures_balance_summary": "futures_balance_summary",
|
||||
*/
|
||||
}
|
||||
|
||||
var defaultSubscriptions = subscription.List{
|
||||
{Enabled: true, Channel: subscription.HeartbeatChannel},
|
||||
{Enabled: true, Asset: asset.All, Channel: "status"},
|
||||
{Enabled: true, Asset: asset.Spot, Channel: subscription.TickerChannel},
|
||||
{Enabled: true, Asset: asset.Spot, Channel: subscription.CandlesChannel},
|
||||
{Enabled: true, Asset: asset.Spot, Channel: subscription.AllTradesChannel},
|
||||
{Enabled: true, Asset: asset.Spot, Channel: subscription.OrderbookChannel},
|
||||
{Enabled: true, Asset: asset.All, Channel: subscription.MyAccountChannel, Authenticated: true},
|
||||
{Enabled: true, Asset: asset.Spot, Channel: "ticker_batch"},
|
||||
/* Not Implemented:
|
||||
{Enabled: false, Asset: asset.Spot, Channel: "futures_balance_summary", Authenticated: true},
|
||||
*/
|
||||
}
|
||||
|
||||
// WsConnect initiates a websocket connection
|
||||
func (e *Exchange) WsConnect() error {
|
||||
ctx := context.TODO()
|
||||
if !e.Websocket.IsEnabled() || !e.IsEnabled() {
|
||||
return websocket.ErrWebsocketNotEnabled
|
||||
}
|
||||
var dialer gws.Dialer
|
||||
if err := e.Websocket.Conn.Dial(ctx, &dialer, http.Header{}); err != nil {
|
||||
return err
|
||||
}
|
||||
e.Websocket.Wg.Add(1)
|
||||
go e.wsReadData()
|
||||
return nil
|
||||
}
|
||||
|
||||
// wsReadData receives and passes on websocket messages for processing
|
||||
func (e *Exchange) wsReadData() {
|
||||
defer e.Websocket.Wg.Done()
|
||||
var seqCount uint64
|
||||
for {
|
||||
resp := e.Websocket.Conn.ReadMessage()
|
||||
if resp.Raw == nil {
|
||||
return
|
||||
}
|
||||
sequence, err := e.wsHandleData(resp.Raw)
|
||||
if err != nil {
|
||||
e.Websocket.DataHandler <- err
|
||||
}
|
||||
if sequence != nil {
|
||||
if *sequence != seqCount {
|
||||
e.Websocket.DataHandler <- fmt.Errorf("%w: received %v, expected %v", errOutOfSequence, sequence, seqCount)
|
||||
seqCount = *sequence
|
||||
}
|
||||
seqCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// wsProcessTicker handles ticker data from the websocket
|
||||
func (e *Exchange) wsProcessTicker(resp *StandardWebsocketResponse) error {
|
||||
var wsTickers []WebsocketTickerHolder
|
||||
if err := json.Unmarshal(resp.Events, &wsTickers); err != nil {
|
||||
return err
|
||||
}
|
||||
var allTickers []ticker.Price
|
||||
aliases := e.pairAliases.GetAliases()
|
||||
for i := range wsTickers {
|
||||
for j := range wsTickers[i].Tickers {
|
||||
symbolAliases := aliases[wsTickers[i].Tickers[j].ProductID]
|
||||
t := ticker.Price{
|
||||
LastUpdated: resp.Timestamp,
|
||||
AssetType: asset.Spot,
|
||||
ExchangeName: e.Name,
|
||||
High: wsTickers[i].Tickers[j].High24H.Float64(),
|
||||
Low: wsTickers[i].Tickers[j].Low24H.Float64(),
|
||||
Last: wsTickers[i].Tickers[j].Price.Float64(),
|
||||
Volume: wsTickers[i].Tickers[j].Volume24H.Float64(),
|
||||
Bid: wsTickers[i].Tickers[j].BestBid.Float64(),
|
||||
BidSize: wsTickers[i].Tickers[j].BestBidQuantity.Float64(),
|
||||
Ask: wsTickers[i].Tickers[j].BestAsk.Float64(),
|
||||
AskSize: wsTickers[i].Tickers[j].BestAskQuantity.Float64(),
|
||||
}
|
||||
var errs error
|
||||
for k := range symbolAliases {
|
||||
if isEnabled, err := e.CurrencyPairs.IsPairEnabled(symbolAliases[k], asset.Spot); err != nil {
|
||||
errs = common.AppendError(errs, err)
|
||||
continue
|
||||
} else if isEnabled {
|
||||
t.Pair = symbolAliases[k]
|
||||
allTickers = append(allTickers, t)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
e.Websocket.DataHandler <- allTickers
|
||||
return nil
|
||||
}
|
||||
|
||||
// wsProcessCandle handles candle data from the websocket
|
||||
func (e *Exchange) wsProcessCandle(resp *StandardWebsocketResponse) error {
|
||||
var wsCandles []WebsocketCandleHolder
|
||||
if err := json.Unmarshal(resp.Events, &wsCandles); err != nil {
|
||||
return err
|
||||
}
|
||||
var allCandles []websocket.KlineData
|
||||
for i := range wsCandles {
|
||||
for j := range wsCandles[i].Candles {
|
||||
allCandles = append(allCandles, websocket.KlineData{
|
||||
Timestamp: resp.Timestamp,
|
||||
Pair: wsCandles[i].Candles[j].ProductID,
|
||||
AssetType: asset.Spot,
|
||||
Exchange: e.Name,
|
||||
StartTime: wsCandles[i].Candles[j].Start.Time(),
|
||||
OpenPrice: wsCandles[i].Candles[j].Open.Float64(),
|
||||
ClosePrice: wsCandles[i].Candles[j].Close.Float64(),
|
||||
HighPrice: wsCandles[i].Candles[j].High.Float64(),
|
||||
LowPrice: wsCandles[i].Candles[j].Low.Float64(),
|
||||
Volume: wsCandles[i].Candles[j].Volume.Float64(),
|
||||
})
|
||||
}
|
||||
}
|
||||
e.Websocket.DataHandler <- allCandles
|
||||
return nil
|
||||
}
|
||||
|
||||
// wsProcessMarketTrades handles market trades data from the websocket
|
||||
func (e *Exchange) wsProcessMarketTrades(resp *StandardWebsocketResponse) error {
|
||||
var wsTrades []WebsocketMarketTradeHolder
|
||||
if err := json.Unmarshal(resp.Events, &wsTrades); err != nil {
|
||||
return err
|
||||
}
|
||||
var allTrades []trade.Data
|
||||
for i := range wsTrades {
|
||||
for j := range wsTrades[i].Trades {
|
||||
allTrades = append(allTrades, trade.Data{
|
||||
TID: wsTrades[i].Trades[j].TradeID,
|
||||
Exchange: e.Name,
|
||||
CurrencyPair: wsTrades[i].Trades[j].ProductID,
|
||||
AssetType: asset.Spot,
|
||||
Side: wsTrades[i].Trades[j].Side,
|
||||
Price: wsTrades[i].Trades[j].Price.Float64(),
|
||||
Amount: wsTrades[i].Trades[j].Size.Float64(),
|
||||
Timestamp: wsTrades[i].Trades[j].Time,
|
||||
})
|
||||
}
|
||||
}
|
||||
e.Websocket.DataHandler <- allTrades
|
||||
return nil
|
||||
}
|
||||
|
||||
// wsProcessL2 handles l2 orderbook data from the websocket
|
||||
func (e *Exchange) wsProcessL2(resp *StandardWebsocketResponse) error {
|
||||
var wsL2 []WebsocketOrderbookDataHolder
|
||||
err := json.Unmarshal(resp.Events, &wsL2)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i := range wsL2 {
|
||||
switch wsL2[i].Type {
|
||||
case "snapshot":
|
||||
err = e.ProcessSnapshot(&wsL2[i], resp.Timestamp)
|
||||
case "update":
|
||||
err = e.ProcessUpdate(&wsL2[i], resp.Timestamp)
|
||||
default:
|
||||
err = fmt.Errorf("%w %v", errUnknownL2DataType, wsL2[i].Type)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// wsProcessUser handles user data from the websocket
|
||||
func (e *Exchange) wsProcessUser(resp *StandardWebsocketResponse) error {
|
||||
var wsUser []WebsocketOrderDataHolder
|
||||
err := json.Unmarshal(resp.Events, &wsUser)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var allOrders []order.Detail
|
||||
for i := range wsUser {
|
||||
for j := range wsUser[i].Orders {
|
||||
var oType order.Type
|
||||
if oType, err = stringToStandardType(wsUser[i].Orders[j].OrderType); err != nil {
|
||||
e.Websocket.DataHandler <- order.ClassificationError{
|
||||
Exchange: e.Name,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
var oSide order.Side
|
||||
if oSide, err = order.StringToOrderSide(wsUser[i].Orders[j].OrderSide); err != nil {
|
||||
e.Websocket.DataHandler <- order.ClassificationError{
|
||||
Exchange: e.Name,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
var oStatus order.Status
|
||||
if oStatus, err = statusToStandardStatus(wsUser[i].Orders[j].Status); err != nil {
|
||||
e.Websocket.DataHandler <- order.ClassificationError{
|
||||
Exchange: e.Name,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
price := wsUser[i].Orders[j].AveragePrice
|
||||
if wsUser[i].Orders[j].LimitPrice != 0 {
|
||||
price = wsUser[i].Orders[j].LimitPrice
|
||||
}
|
||||
var assetType asset.Item
|
||||
if assetType, err = stringToStandardAsset(wsUser[i].Orders[j].ProductType); err != nil {
|
||||
e.Websocket.DataHandler <- order.ClassificationError{
|
||||
Exchange: e.Name,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
var tif order.TimeInForce
|
||||
if tif, err = strategyDecoder(wsUser[i].Orders[j].TimeInForce); err != nil {
|
||||
e.Websocket.DataHandler <- order.ClassificationError{
|
||||
Exchange: e.Name,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
if wsUser[i].Orders[j].PostOnly {
|
||||
tif |= order.PostOnly
|
||||
}
|
||||
allOrders = append(allOrders, order.Detail{
|
||||
Price: price.Float64(),
|
||||
ClientOrderID: wsUser[i].Orders[j].ClientOrderID,
|
||||
ExecutedAmount: wsUser[i].Orders[j].CumulativeQuantity.Float64(),
|
||||
RemainingAmount: wsUser[i].Orders[j].LeavesQuantity.Float64(),
|
||||
Amount: wsUser[i].Orders[j].CumulativeQuantity.Float64() + wsUser[i].Orders[j].LeavesQuantity.Float64(),
|
||||
OrderID: wsUser[i].Orders[j].OrderID,
|
||||
Side: oSide,
|
||||
Type: oType,
|
||||
Pair: wsUser[i].Orders[j].ProductID,
|
||||
AssetType: assetType,
|
||||
Status: oStatus,
|
||||
TriggerPrice: wsUser[i].Orders[j].StopPrice.Float64(),
|
||||
TimeInForce: tif,
|
||||
Fee: wsUser[i].Orders[j].TotalFees.Float64(),
|
||||
Date: wsUser[i].Orders[j].CreationTime,
|
||||
CloseTime: wsUser[i].Orders[j].EndTime,
|
||||
Exchange: e.Name,
|
||||
})
|
||||
}
|
||||
for j := range wsUser[i].Positions.PerpetualFuturesPositions {
|
||||
var oSide order.Side
|
||||
if oSide, err = order.StringToOrderSide(wsUser[i].Positions.PerpetualFuturesPositions[j].PositionSide); err != nil {
|
||||
e.Websocket.DataHandler <- order.ClassificationError{
|
||||
Exchange: e.Name,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
var mType margin.Type
|
||||
if mType, err = margin.StringToMarginType(wsUser[i].Positions.PerpetualFuturesPositions[j].MarginType); err != nil {
|
||||
e.Websocket.DataHandler <- order.ClassificationError{
|
||||
Exchange: e.Name,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
allOrders = append(allOrders, order.Detail{
|
||||
Pair: wsUser[i].Positions.PerpetualFuturesPositions[j].ProductID,
|
||||
Side: oSide,
|
||||
MarginType: mType,
|
||||
Amount: wsUser[i].Positions.PerpetualFuturesPositions[j].NetSize.Float64(),
|
||||
Leverage: wsUser[i].Positions.PerpetualFuturesPositions[j].Leverage.Float64(),
|
||||
AssetType: asset.Futures,
|
||||
Exchange: e.Name,
|
||||
})
|
||||
}
|
||||
for j := range wsUser[i].Positions.ExpiringFuturesPositions {
|
||||
var oSide order.Side
|
||||
if oSide, err = order.StringToOrderSide(wsUser[i].Positions.ExpiringFuturesPositions[j].Side); err != nil {
|
||||
e.Websocket.DataHandler <- order.ClassificationError{
|
||||
Exchange: e.Name,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
allOrders = append(allOrders, order.Detail{
|
||||
Pair: wsUser[i].Positions.ExpiringFuturesPositions[j].ProductID,
|
||||
Side: oSide,
|
||||
ContractAmount: wsUser[i].Positions.ExpiringFuturesPositions[j].NumberOfContracts.Float64(),
|
||||
Price: wsUser[i].Positions.ExpiringFuturesPositions[j].EntryPrice.Float64(),
|
||||
})
|
||||
}
|
||||
}
|
||||
e.Websocket.DataHandler <- allOrders
|
||||
return nil
|
||||
}
|
||||
|
||||
// wsHandleData handles all the websocket data coming from the websocket connection
|
||||
func (e *Exchange) wsHandleData(respRaw []byte) (*uint64, error) {
|
||||
var resp StandardWebsocketResponse
|
||||
if err := json.Unmarshal(respRaw, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.Error != "" {
|
||||
return &resp.Sequence, errors.New(resp.Error)
|
||||
}
|
||||
switch resp.Channel {
|
||||
case "subscriptions", "heartbeats":
|
||||
return &resp.Sequence, nil
|
||||
case "status":
|
||||
var wsStatus []WebsocketProductHolder
|
||||
if err := json.Unmarshal(resp.Events, &wsStatus); err != nil {
|
||||
return &resp.Sequence, err
|
||||
}
|
||||
e.Websocket.DataHandler <- wsStatus
|
||||
case "ticker", "ticker_batch":
|
||||
if err := e.wsProcessTicker(&resp); err != nil {
|
||||
return &resp.Sequence, err
|
||||
}
|
||||
case "candles":
|
||||
if err := e.wsProcessCandle(&resp); err != nil {
|
||||
return &resp.Sequence, err
|
||||
}
|
||||
case "market_trades":
|
||||
if err := e.wsProcessMarketTrades(&resp); err != nil {
|
||||
return &resp.Sequence, err
|
||||
}
|
||||
case "l2_data":
|
||||
if err := e.wsProcessL2(&resp); err != nil {
|
||||
return &resp.Sequence, err
|
||||
}
|
||||
case "user":
|
||||
if err := e.wsProcessUser(&resp); err != nil {
|
||||
return &resp.Sequence, err
|
||||
}
|
||||
default:
|
||||
return &resp.Sequence, errChannelNameUnknown
|
||||
}
|
||||
return &resp.Sequence, nil
|
||||
}
|
||||
|
||||
// ProcessSnapshot processes the initial orderbook snap shot
|
||||
func (e *Exchange) ProcessSnapshot(snapshot *WebsocketOrderbookDataHolder, timestamp time.Time) error {
|
||||
bids, asks, err := processBidAskArray(snapshot, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
book := &orderbook.Book{
|
||||
Bids: bids,
|
||||
Asks: asks,
|
||||
Exchange: e.Name,
|
||||
Pair: snapshot.ProductID,
|
||||
Asset: asset.Spot,
|
||||
LastUpdated: timestamp,
|
||||
ValidateOrderbook: e.ValidateOrderbook,
|
||||
}
|
||||
for _, a := range e.pairAliases.GetAlias(snapshot.ProductID) {
|
||||
isEnabled, err := e.IsPairEnabled(a, asset.Spot)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if isEnabled {
|
||||
book.Pair = a
|
||||
if err := e.Websocket.Orderbook.LoadSnapshot(book); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ProcessUpdate updates the orderbook local cache
|
||||
func (e *Exchange) ProcessUpdate(update *WebsocketOrderbookDataHolder, timestamp time.Time) error {
|
||||
bids, asks, err := processBidAskArray(update, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
obU := &orderbook.Update{
|
||||
Bids: bids,
|
||||
Asks: asks,
|
||||
Pair: update.ProductID,
|
||||
UpdateTime: timestamp,
|
||||
Asset: asset.Spot,
|
||||
}
|
||||
for _, a := range e.pairAliases.GetAlias(update.ProductID) {
|
||||
isEnabled, err := e.IsPairEnabled(a, asset.Spot)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if isEnabled {
|
||||
obU.Pair = a
|
||||
if err := e.Websocket.Orderbook.Update(obU); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GenerateSubscriptions adds default subscriptions to websocket to be handled by ManageSubscriptions()
|
||||
func (e *Exchange) generateSubscriptions() (subscription.List, error) {
|
||||
return e.Features.Subscriptions.ExpandTemplates(e)
|
||||
}
|
||||
|
||||
// GetSubscriptionTemplate returns a subscription channel template
|
||||
func (e *Exchange) GetSubscriptionTemplate(_ *subscription.Subscription) (*template.Template, error) {
|
||||
return template.New("master.tmpl").Funcs(template.FuncMap{"channelName": channelName}).Parse(subTplText)
|
||||
}
|
||||
|
||||
// Subscribe sends a websocket message to receive data from a list of channels
|
||||
func (e *Exchange) Subscribe(subs subscription.List) error {
|
||||
return e.ParallelChanOp(context.TODO(), subs, func(ctx context.Context, subs subscription.List) error { return e.manageSubs(ctx, "subscribe", subs) }, 1)
|
||||
}
|
||||
|
||||
// Unsubscribe sends a websocket message to stop receiving data from a list of channels
|
||||
func (e *Exchange) Unsubscribe(subs subscription.List) error {
|
||||
return e.ParallelChanOp(context.TODO(), subs, func(ctx context.Context, subs subscription.List) error { return e.manageSubs(ctx, "unsubscribe", subs) }, 1)
|
||||
}
|
||||
|
||||
// manageSubs subscribes or unsubscribes from a list of websocket channels
|
||||
func (e *Exchange) manageSubs(ctx context.Context, op string, subs subscription.List) error {
|
||||
var errs error
|
||||
subs, errs = subs.ExpandTemplates(e)
|
||||
for _, s := range subs {
|
||||
r := &WebsocketRequest{
|
||||
Type: op,
|
||||
ProductIDs: s.Pairs,
|
||||
Channel: s.QualifiedChannel,
|
||||
Timestamp: strconv.FormatInt(time.Now().Unix(), 10),
|
||||
}
|
||||
var err error
|
||||
limitType := WSUnauthRate
|
||||
if s.Authenticated {
|
||||
limitType = WSAuthRate
|
||||
if r.JWT, err = e.GetWSJWT(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err = e.Websocket.Conn.SendJSONMessage(ctx, limitType, r); err == nil {
|
||||
switch op {
|
||||
case "subscribe":
|
||||
err = e.Websocket.AddSuccessfulSubscriptions(e.Websocket.Conn, s)
|
||||
case "unsubscribe":
|
||||
err = e.Websocket.RemoveSubscriptions(e.Websocket.Conn, s)
|
||||
}
|
||||
}
|
||||
errs = common.AppendError(errs, err)
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
// GetWSJWT returns a JWT, using a stored one of it's provided, and generating a new one otherwise
|
||||
func (e *Exchange) GetWSJWT(ctx context.Context) (string, error) {
|
||||
e.jwt.m.RLock()
|
||||
if e.jwt.expiresAt.After(time.Now()) {
|
||||
retStr := e.jwt.token
|
||||
e.jwt.m.RUnlock()
|
||||
return retStr, nil
|
||||
}
|
||||
e.jwt.m.RUnlock()
|
||||
e.jwt.m.Lock()
|
||||
defer e.jwt.m.Unlock()
|
||||
var err error
|
||||
e.jwt.token, e.jwt.expiresAt, err = e.GetJWT(ctx, "")
|
||||
return e.jwt.token, err
|
||||
}
|
||||
|
||||
// processBidAskArray is a helper function that turns WebsocketOrderbookDataHolder into arrays of bids and asks
|
||||
func processBidAskArray(data *WebsocketOrderbookDataHolder, snapshot bool) (bids, asks orderbook.Levels, err error) {
|
||||
bids = make(orderbook.Levels, 0, len(data.Changes))
|
||||
asks = make(orderbook.Levels, 0, len(data.Changes))
|
||||
for i := range data.Changes {
|
||||
change := orderbook.Level{Price: data.Changes[i].PriceLevel.Float64(), Amount: data.Changes[i].NewQuantity.Float64()}
|
||||
switch data.Changes[i].Side {
|
||||
case "bid":
|
||||
bids = append(bids, change)
|
||||
case "offer":
|
||||
asks = append(asks, change)
|
||||
default:
|
||||
return nil, nil, fmt.Errorf("%w %v", order.ErrSideIsInvalid, data.Changes[i].Side)
|
||||
}
|
||||
}
|
||||
if snapshot {
|
||||
return slices.Clip(bids), slices.Clip(asks), nil
|
||||
}
|
||||
return bids, asks, nil
|
||||
}
|
||||
|
||||
// statusToStandardStatus is a helper function that converts a Coinbase Pro status string to a standardised order.Status type
|
||||
func statusToStandardStatus(stat string) (order.Status, error) {
|
||||
switch stat {
|
||||
case "PENDING":
|
||||
return order.New, nil
|
||||
case "OPEN":
|
||||
return order.Active, nil
|
||||
case "FILLED":
|
||||
return order.Filled, nil
|
||||
case "CANCELLED":
|
||||
return order.Cancelled, nil
|
||||
case "EXPIRED":
|
||||
return order.Expired, nil
|
||||
case "FAILED":
|
||||
return order.Rejected, nil
|
||||
default:
|
||||
return order.UnknownStatus, fmt.Errorf("%w %v", order.ErrUnsupportedStatusType, stat)
|
||||
}
|
||||
}
|
||||
|
||||
// stringToStandardType is a helper function that converts a Coinbase Pro side string to a standardised order.Type type
|
||||
func stringToStandardType(str string) (order.Type, error) {
|
||||
switch str {
|
||||
case "LIMIT_ORDER_TYPE":
|
||||
return order.Limit, nil
|
||||
case "MARKET_ORDER_TYPE":
|
||||
return order.Market, nil
|
||||
case "STOP_LIMIT_ORDER_TYPE":
|
||||
return order.StopLimit, nil
|
||||
default:
|
||||
return order.UnknownType, fmt.Errorf("%w %v", order.ErrUnrecognisedOrderType, str)
|
||||
}
|
||||
}
|
||||
|
||||
// stringToStandardAsset is a helper function that converts a Coinbase Pro asset string to a standardised asset.Item type
|
||||
func stringToStandardAsset(str string) (asset.Item, error) {
|
||||
switch str {
|
||||
case "SPOT":
|
||||
return asset.Spot, nil
|
||||
case "FUTURE":
|
||||
return asset.Futures, nil
|
||||
default:
|
||||
return asset.Empty, asset.ErrNotSupported
|
||||
}
|
||||
}
|
||||
|
||||
// strategyDecoder is a helper function that converts a Coinbase Pro time in force string to a few standardised bools
|
||||
func strategyDecoder(str string) (tif order.TimeInForce, err error) {
|
||||
switch str {
|
||||
case "IMMEDIATE_OR_CANCEL":
|
||||
return order.ImmediateOrCancel, nil
|
||||
case "FILL_OR_KILL":
|
||||
return order.FillOrKill, nil
|
||||
case "GOOD_UNTIL_CANCELLED":
|
||||
return order.GoodTillCancel, nil
|
||||
case "GOOD_UNTIL_DATE_TIME":
|
||||
return order.GoodTillDay | order.GoodTillTime, nil
|
||||
default:
|
||||
return order.UnknownTIF, fmt.Errorf("%w %v", errUnrecognisedStrategyType, str)
|
||||
}
|
||||
}
|
||||
|
||||
// checkSubscriptions looks for incompatible subscriptions and if found replaces all with defaults
|
||||
// This should be unnecessary and removable by mid-2025
|
||||
func (e *Exchange) checkSubscriptions() {
|
||||
for _, s := range e.Config.Features.Subscriptions {
|
||||
switch s.Channel {
|
||||
case "level2_batch", "matches":
|
||||
e.Config.Features.Subscriptions = defaultSubscriptions.Clone()
|
||||
e.Features.Subscriptions = e.Config.Features.Subscriptions.Enabled()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func channelName(s *subscription.Subscription) (string, error) {
|
||||
if n, ok := subscriptionNames[s.Channel]; ok {
|
||||
return n, nil
|
||||
}
|
||||
return "", fmt.Errorf("%w: %s", subscription.ErrNotSupported, s.Channel)
|
||||
}
|
||||
|
||||
const subTplText = `
|
||||
{{ range $asset, $pairs := $.AssetPairs }}
|
||||
{{- channelName $.S -}}
|
||||
{{- $.AssetSeparator }}
|
||||
{{- end }}
|
||||
`
|
||||
1206
exchanges/coinbase/coinbase_wrapper.go
Normal file
1206
exchanges/coinbase/coinbase_wrapper.go
Normal file
File diff suppressed because it is too large
Load Diff
24
exchanges/coinbase/ratelimit.go
Normal file
24
exchanges/coinbase/ratelimit.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package coinbase
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
|
||||
)
|
||||
|
||||
// Coinbase pro rate limits
|
||||
const (
|
||||
V2Rate request.EndpointLimit = iota
|
||||
V3Rate
|
||||
WSAuthRate
|
||||
WSUnauthRate
|
||||
PubRate
|
||||
)
|
||||
|
||||
var rateLimits = request.RateLimitDefinitions{
|
||||
V2Rate: request.NewRateLimitWithWeight(time.Hour, 10000, 1),
|
||||
V3Rate: request.NewRateLimitWithWeight(time.Second, 27, 1),
|
||||
WSAuthRate: request.NewRateLimitWithWeight(time.Second, 750, 1),
|
||||
WSUnauthRate: request.NewRateLimitWithWeight(time.Second, 8, 1),
|
||||
PubRate: request.NewRateLimitWithWeight(time.Second, 10, 1),
|
||||
}
|
||||
Reference in New Issue
Block a user