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:
cranktakular
2025-09-16 13:37:00 +10:00
committed by GitHub
parent d957ddae62
commit fd9aaf00a2
78 changed files with 8850 additions and 3937 deletions

View File

@@ -28,7 +28,7 @@ Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader
| BTCMarkets | Yes | Yes | NA | | BTCMarkets | Yes | Yes | NA |
| BTSE | Yes | Yes | NA | | BTSE | Yes | Yes | NA |
| Bybit | Yes | Yes | NA | | Bybit | Yes | Yes | NA |
| CoinbasePro | Yes | Yes | No| | Coinbase | Yes | Yes | No|
| COINUT | Yes | Yes | NA | | COINUT | Yes | Yes | NA |
| Deribit | Yes | Yes | No | | Deribit | Yes | Yes | No |
| Exmo | Yes | NA | NA | | Exmo | Yes | NA | NA |

View File

@@ -202,7 +202,7 @@ func TestPlaceOrder(t *testing.T) {
Base: &event.Base{}, Base: &event.Base{},
} }
_, err = e.placeOrder(t.Context(), decimal.NewFromInt(1), decimal.NewFromInt(1), decimal.Zero, false, true, f, bot.OrderManager) _, err = e.placeOrder(t.Context(), decimal.NewFromInt(1), decimal.NewFromInt(1), decimal.Zero, false, true, f, bot.OrderManager)
assert.ErrorIs(t, err, engine.ErrExchangeNameIsEmpty) assert.ErrorIs(t, err, gctcommon.ErrExchangeNameNotSet)
f.Exchange = testExchange f.Exchange = testExchange
require.NoError(t, exch.UpdateOrderExecutionLimits(t.Context(), asset.Spot), "UpdateOrderExecutionLimits must not error") require.NoError(t, exch.UpdateOrderExecutionLimits(t.Context(), asset.Spot), "UpdateOrderExecutionLimits must not error")

View File

@@ -801,7 +801,7 @@ func (f *FundManager) HasExchangeBeenLiquidated(ev common.Event) bool {
// help calculate collateral // help calculate collateral
func (f *FundManager) SetFunding(exchName string, item asset.Item, balance *account.Balance, initialFundsSet bool) error { func (f *FundManager) SetFunding(exchName string, item asset.Item, balance *account.Balance, initialFundsSet bool) error {
if exchName == "" { if exchName == "" {
return engine.ErrExchangeNameIsEmpty return gctcommon.ErrExchangeNameNotSet
} }
if !item.IsValid() { if !item.IsValid() {
return asset.ErrNotSupported return asset.ErrNotSupported

View File

@@ -709,7 +709,7 @@ func TestSetFunding(t *testing.T) {
t.Parallel() t.Parallel()
f := &FundManager{} f := &FundManager{}
err := f.SetFunding("", 0, nil, false) err := f.SetFunding("", 0, nil, false)
assert.ErrorIs(t, err, engine.ErrExchangeNameIsEmpty) assert.ErrorIs(t, err, gctcommon.ErrExchangeNameNotSet)
err = f.SetFunding(exchName, 0, nil, false) err = f.SetFunding(exchName, 0, nil, false)
assert.ErrorIs(t, err, asset.ErrNotSupported) assert.ErrorIs(t, err, asset.ErrNotSupported)

View File

@@ -1,6 +1,6 @@
{{define "exchanges coinbasepro" -}} {{define "exchanges coinbase" -}}
{{template "header" .}} {{template "header" .}}
## CoinbasePro Exchange ## Coinbase Exchange
### Current Features ### Current Features
@@ -31,7 +31,7 @@ main.go
var c exchange.IBotExchange var c exchange.IBotExchange
for i := range bot.Exchanges { for i := range bot.Exchanges {
if bot.Exchanges[i].GetName() == "CoinbasePro" { if bot.Exchanges[i].GetName() == "Coinbase" {
c = bot.Exchanges[i] c = bot.Exchanges[i]
} }
} }

View File

@@ -51,7 +51,7 @@ _b in this context is an `IBotExchange` implemented struct_
| BTCMarkets | Yes | Yes | No | | BTCMarkets | Yes | Yes | No |
| BTSE | Yes | Yes | No | | BTSE | Yes | Yes | No |
| Bybit | Yes | Yes | Yes | | Bybit | Yes | Yes | Yes |
| CoinbasePro | Yes | Yes | No| | Coinbase | Yes | Yes | No|
| COINUT | Yes | Yes | No | | COINUT | Yes | Yes | No |
| Deribit | Yes | Yes | Yes | | Deribit | Yes | Yes | Yes |
| Exmo | Yes | NA | No | | Exmo | Yes | NA | No |

View File

@@ -29,7 +29,7 @@ Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader
| BTCMarkets | Yes | Yes | NA | | BTCMarkets | Yes | Yes | NA |
| BTSE | Yes | Yes | NA | | BTSE | Yes | Yes | NA |
| Bybit | Yes | Yes | NA | | Bybit | Yes | Yes | NA |
| CoinbasePro | Yes | Yes | No| | Coinbase | Yes | Yes | No|
| COINUT | Yes | Yes | NA | | COINUT | Yes | Yes | NA |
| Deribit | Yes | Yes | No | | Deribit | Yes | Yes | No |
| Exmo | Yes | NA | NA | | Exmo | Yes | NA | NA |

View File

@@ -92,22 +92,18 @@ func setupExchange(ctx context.Context, t *testing.T, name string, cfg *config.C
exch.SetDefaults() exch.SetDefaults()
exchCfg.API.AuthenticatedSupport = true exchCfg.API.AuthenticatedSupport = true
exchCfg.API.Credentials = getExchangeCredentials(name) exchCfg.API.Credentials = getExchangeCredentials(name)
err = exch.Setup(exchCfg) err = exch.Setup(exchCfg)
if err != nil { if err != nil {
t.Fatalf("Cannot setup %v exchange Setup %v", name, err) t.Fatalf("Cannot setup %v exchange Setup %v", name, err)
} }
err = exch.UpdateTradablePairs(ctx, true) err = exch.UpdateTradablePairs(ctx, true)
require.Truef(t, errors.Is(err, context.DeadlineExceeded) || err == nil, "Exchange %s UpdateTradablePairs must not error: %s", name, err) require.Truef(t, errors.Is(err, context.DeadlineExceeded) || err == nil, "Exchange %s UpdateTradablePairs must not error: %s", name, err)
b := exch.GetBase() b := exch.GetBase()
assets := b.CurrencyPairs.GetAssetTypes(false) assets := b.CurrencyPairs.GetAssetTypes(false)
require.NotEmptyf(t, assets, "Exchange %s must have assets", name) require.NotEmptyf(t, assets, "Exchange %s must have assets", name)
for _, a := range assets { for _, a := range assets {
require.NoErrorf(t, b.CurrencyPairs.SetAssetEnabled(a, true), "Exchange %s SetAssetEnabled must not error for asset %s: %s", name, a, err) require.NoErrorf(t, b.CurrencyPairs.SetAssetEnabled(a, true), "Exchange %s SetAssetEnabled must not error for asset %s: %s", name, a, err)
} }
// Add +1 to len to verify that exchanges can handle requests with unset pairs and assets // Add +1 to len to verify that exchanges can handle requests with unset pairs and assets
assetPairs := make([]assetPair, 0, len(assets)+1) assetPairs := make([]assetPair, 0, len(assets)+1)
assets: assets:
@@ -147,7 +143,6 @@ assets:
}) })
} }
assetPairs = append(assetPairs, assetPair{}) assetPairs = append(assetPairs, assetPair{})
return exch, assetPairs return exch, assetPairs
} }
@@ -191,7 +186,6 @@ func executeExchangeWrapperTests(ctx context.Context, t *testing.T, exch exchang
continue continue
} }
method := actualExchange.MethodByName(methodName) method := actualExchange.MethodByName(methodName)
var assetLen int var assetLen int
for y := range method.Type().NumIn() { for y := range method.Type().NumIn() {
input := method.Type().In(y) input := method.Type().In(y)
@@ -391,6 +385,7 @@ func generateMethodArg(ctx context.Context, t *testing.T, argGenerator *MethodAr
Description: "1337", Description: "1337",
Amount: 1, Amount: 1,
ClientOrderID: "1337", ClientOrderID: "1337",
WalletID: "7331",
} }
if argGenerator.MethodName == "WithdrawCryptocurrencyFunds" { if argGenerator.MethodName == "WithdrawCryptocurrencyFunds" {
req.Type = withdraw.Crypto req.Type = withdraw.Crypto
@@ -614,10 +609,9 @@ var unsupportedAssets = []asset.Item{
var unsupportedExchangeNames = []string{ var unsupportedExchangeNames = []string{
"testexch", "testexch",
"bitflyer", // Bitflyer has many "ErrNotYetImplemented, which is true, but not what we care to test for here "bitflyer", // Bitflyer has many "ErrNotYetImplemented, which is true, but not what we care to test for here
"btse", // TODO rm once timeout issues resolved "btse", // TODO rm once timeout issues resolved
"poloniex", // outdated API // TODO rm once updated "poloniex", // outdated API // TODO rm once updated
"coinbasepro", // outdated API // TODO rm once updated
} }
// cryptoChainPerExchange holds the deposit address chain per exchange // cryptoChainPerExchange holds the deposit address chain per exchange

View File

@@ -55,6 +55,7 @@ var (
// Public common Errors // Public common Errors
var ( var (
ErrExchangeNameNotSet = errors.New("exchange name not set")
ErrNotYetImplemented = errors.New("not yet implemented") ErrNotYetImplemented = errors.New("not yet implemented")
ErrFunctionNotSupported = errors.New("unsupported wrapper function") ErrFunctionNotSupported = errors.New("unsupported wrapper function")
ErrAddressIsEmptyOrInvalid = errors.New("address is empty or invalid") ErrAddressIsEmptyOrInvalid = errors.New("address is empty or invalid")

View File

@@ -916,7 +916,7 @@ func (c *Config) CheckExchangeConfigValues() error {
continue continue
} }
if e.Name == "" { if e.Name == "" {
log.Errorf(log.ConfigMgr, "%s: #%d", errExchangeNameEmpty, i) log.Errorf(log.ConfigMgr, "%s: #%d", common.ErrExchangeNameNotSet, i)
e.Enabled = false e.Enabled = false
continue continue
} }

View File

@@ -83,7 +83,6 @@ var (
errNoEnabledExchanges = errors.New("no exchanges enabled") errNoEnabledExchanges = errors.New("no exchanges enabled")
errCheckingConfigValues = errors.New("fatal error checking config values") errCheckingConfigValues = errors.New("fatal error checking config values")
errExchangeNameEmpty = errors.New("exchange name is empty")
) )
// Config is the overarching object that holds all the information for // Config is the overarching object that holds all the information for

View File

@@ -10,6 +10,7 @@ import (
v6 "github.com/thrasher-corp/gocryptotrader/config/versions/v6" v6 "github.com/thrasher-corp/gocryptotrader/config/versions/v6"
v7 "github.com/thrasher-corp/gocryptotrader/config/versions/v7" v7 "github.com/thrasher-corp/gocryptotrader/config/versions/v7"
v8 "github.com/thrasher-corp/gocryptotrader/config/versions/v8" v8 "github.com/thrasher-corp/gocryptotrader/config/versions/v8"
v9 "github.com/thrasher-corp/gocryptotrader/config/versions/v9"
) )
func init() { func init() {
@@ -22,4 +23,5 @@ func init() {
Manager.registerVersion(6, &v6.Version{}) Manager.registerVersion(6, &v6.Version{})
Manager.registerVersion(7, &v7.Version{}) Manager.registerVersion(7, &v7.Version{})
Manager.registerVersion(8, &v8.Version{}) Manager.registerVersion(8, &v8.Version{})
Manager.registerVersion(9, &v9.Version{})
} }

29
config/versions/v9/v9.go Normal file
View File

@@ -0,0 +1,29 @@
package v9
import (
"context"
"github.com/buger/jsonparser"
)
// Version is an ExchangeVersion to change the name of CoinbasePro to Coinbase
type Version struct{}
// Exchanges returns just CoinbasePro and Coinbase
func (*Version) Exchanges() []string { return []string{"CoinbasePro", "Coinbase"} }
// UpgradeExchange will change the exchange name from CoinbasePro to Coinbase
func (*Version) UpgradeExchange(_ context.Context, e []byte) ([]byte, error) {
if n, err := jsonparser.GetString(e, "name"); err == nil && n == "CoinbasePro" {
return jsonparser.Set(e, []byte(`"Coinbase"`), "name")
}
return e, nil
}
// DowngradeExchange will change the exchange name from Coinbase to CoinbasePro
func (*Version) DowngradeExchange(_ context.Context, e []byte) ([]byte, error) {
if n, err := jsonparser.GetString(e, "name"); err == nil && n == "Coinbase" {
return jsonparser.Set(e, []byte(`"CoinbasePro"`), "name")
}
return e, nil
}

View File

@@ -0,0 +1,37 @@
package v9_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
v9 "github.com/thrasher-corp/gocryptotrader/config/versions/v9"
)
func TestUpgradeExchange(t *testing.T) {
t.Parallel()
for _, tt := range [][]string{
{"CoinbasePro", "Coinbase"},
{"Kraken", "Kraken"},
{"Coinbase", "Coinbase"},
} {
out, err := new(v9.Version).UpgradeExchange(t.Context(), []byte(`{"name":"`+tt[0]+`"}`))
require.NoError(t, err)
require.NotEmpty(t, out)
assert.Equalf(t, `{"name":"`+tt[1]+`"}`, string(out), "Test exchange name %s", tt[0])
}
}
func TestDowngradeExchange(t *testing.T) {
t.Parallel()
for _, tt := range [][]string{
{"Coinbase", "CoinbasePro"},
{"Kraken", "Kraken"},
{"CoinbasePro", "CoinbasePro"},
} {
out, err := new(v9.Version).DowngradeExchange(t.Context(), []byte(`{"name":"`+tt[0]+`"}`))
require.NoError(t, err)
require.NotEmpty(t, out)
assert.Equalf(t, `{"name":"`+tt[1]+`"}`, string(out), "Test exchange name %s", tt[0])
}
}

File diff suppressed because one or more lines are too long

View File

@@ -3021,12 +3021,15 @@ var (
FI = NewCode("FI") FI = NewCode("FI")
USDM = NewCode("USDM") USDM = NewCode("USDM")
USDTM = NewCode("USDTM") USDTM = NewCode("USDTM")
CBETH = NewCode("CBETH")
PYUSD = NewCode("PYUSD")
EUROC = NewCode("EUROC")
LSETH = NewCode("LSETH")
LEVER = NewCode("LEVER") LEVER = NewCode("LEVER")
NESS = NewCode("NESS") NESS = NewCode("NESS")
KAS = NewCode("KAS") KAS = NewCode("KAS")
NEXT = NewCode("NEXT") NEXT = NewCode("NEXT")
VEXT = NewCode("VEXT") VEXT = NewCode("VEXT")
PYUSD = NewCode("PYUSD")
SAIL = NewCode("SAIL") SAIL = NewCode("SAIL")
VV = NewCode("VV") VV = NewCode("VV")
ORDI = NewCode("ORDI") ORDI = NewCode("ORDI")

View File

@@ -4,6 +4,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"maps" "maps"
"slices"
"strings" "strings"
"sync" "sync"
@@ -11,6 +12,11 @@ import (
"github.com/thrasher-corp/gocryptotrader/log" "github.com/thrasher-corp/gocryptotrader/log"
) )
var (
errNoProvider = errors.New("no supporting foreign exchange providers set")
errUnsupportedCurrencies = errors.New("currencies not supported by provider")
)
// IFXProvider enforces standard functions for all foreign exchange providers // IFXProvider enforces standard functions for all foreign exchange providers
// supported in GoCryptoTrader // supported in GoCryptoTrader
type IFXProvider interface { type IFXProvider interface {
@@ -106,47 +112,27 @@ func (f *FXHandler) GetCurrencyData(baseCurrency string, currencies []string) (m
return rates, nil return rates, nil
} }
// backupGetRate uses the currencies that are supported and falls through, and // backupGetRate uses the currencies that are supported and falls through, and errors when unsupported currency found
// errors when unsupported currency found
func (f *FXHandler) backupGetRate(base string, currencies []string) (map[string]float64, error) { func (f *FXHandler) backupGetRate(base string, currencies []string) (map[string]float64, error) {
if f.Support == nil { if f.Support == nil {
return nil, errors.New("no supporting foreign exchange providers set") return nil, errNoProvider
} }
var shunt []string
rate := make(map[string]float64) rate := make(map[string]float64)
for i := range f.Support { for i := range f.Support {
if len(shunt) != 0 { missed := f.Support[i].CheckCurrencies(currencies)
shunt = f.Support[i].CheckCurrencies(shunt) toGet := slices.DeleteFunc(currencies, func(s string) bool {
newRate, err := f.Support[i].GetNewRate(base, shunt) return common.StringSliceContains(missed, s)
if err != nil { })
continue newRate, err := f.Support[i].GetNewRate(base, toGet)
}
maps.Copy(rate, newRate)
if len(shunt) != 0 {
continue
}
return rate, nil
}
shunt = f.Support[i].CheckCurrencies(currencies)
newRate, err := f.Support[i].GetNewRate(base, currencies)
if err != nil { if err != nil {
continue return nil, err
} }
maps.Copy(rate, newRate) maps.Copy(rate, newRate)
if len(missed) != 0 {
if len(shunt) != 0 { currencies = missed
continue continue
} }
return rate, nil return rate, nil
} }
return nil, fmt.Errorf("%w: %s", errUnsupportedCurrencies, currencies)
return nil, fmt.Errorf("currencies %s not supported", shunt)
} }

View File

@@ -0,0 +1,66 @@
package base
import (
"errors"
"math/rand/v2"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
var errCurrencyNotSupported = errors.New("currency not supported")
type MockProvider struct {
IFXProvider
value float64
}
func (m *MockProvider) IsEnabled() bool {
return true
}
func (m *MockProvider) GetName() string {
return ""
}
func (m *MockProvider) GetRates(baseCurrency, symbols string) (map[string]float64, error) {
c := map[string]float64{}
for s := range strings.SplitSeq(symbols, ",") {
if s == "XRP" && m.value == 1.5 {
return nil, errCurrencyNotSupported
}
if s == "BTC" {
c[baseCurrency+s] = m.value
continue
}
c[baseCurrency+s] = 1 / (1 + rand.Float64()) //nolint:gosec // Doesn't need to be a strong random number
}
return c, nil
}
func TestBackupGetRate(t *testing.T) {
var f FXHandler
_, err := f.backupGetRate("", nil)
assert.ErrorIs(t, err, errNoProvider)
f.Support = append(f.Support, Provider{
SupportedCurrencies: []string{"BTC", "ETH", "XRP"},
Provider: &MockProvider{
value: 1.5,
},
}, Provider{
SupportedCurrencies: []string{"BTC", "LTC", "XRP"},
Provider: &MockProvider{
value: 2.5,
},
})
_, err = f.backupGetRate("", []string{"XRP"})
assert.ErrorIs(t, err, errCurrencyNotSupported)
_, err = f.backupGetRate("", []string{"NOTREALCURRENCY"})
assert.ErrorIs(t, err, errUnsupportedCurrencies)
f.Support[0].SupportedCurrencies = []string{"BTC", "ETH"}
r, err := f.backupGetRate("USD", []string{"BTC", "ETH", "LTC", "XRP"})
assert.NoError(t, err)
assert.Len(t, r, 4)
assert.Equal(t, 1.5, r["USDBTC"])
}

View File

@@ -145,7 +145,6 @@ list:
if p.Contains(check[x], exact) { if p.Contains(check[x], exact) {
return fmt.Errorf("%s %w", check[x], ErrPairDuplication) return fmt.Errorf("%s %w", check[x], ErrPairDuplication)
} }
return fmt.Errorf("%s %w", check[x], ErrPairNotContainedInAvailablePairs) return fmt.Errorf("%s %w", check[x], ErrPairNotContainedInAvailablePairs)
} }
return nil return nil

View File

@@ -145,7 +145,7 @@ Similar to the configs, spot support is inbuilt but other asset types will need
| COINUT | Yes | Yes | NA | | COINUT | Yes | Yes | NA |
| Deribit | Yes | Yes | NA | | Deribit | Yes | Yes | NA |
| Exmo | Yes | NA | NA | | Exmo | Yes | NA | NA |
| CoinbasePro | Yes | Yes | No| | Coinbase | Yes | Yes | No|
| GateIO | Yes | Yes | NA | | GateIO | Yes | Yes | NA |
| Gemini | Yes | Yes | No | | Gemini | Yes | Yes | No |
| HitBTC | Yes | Yes | No | | HitBTC | Yes | Yes | No |
@@ -171,7 +171,7 @@ var Exchanges = []string{
"btc markets", "btc markets",
"btse", "btse",
"bybit", "bybit",
"coinbasepro", "coinbase",
"coinut", "coinut",
"deribit", "deribit",
"exmo", "exmo",

View File

@@ -52,7 +52,7 @@ $ ./gctcli withdrawcryptofunds --exchange=binance --currency=USDT --address=TJU9
| BTCMarkets | No | No| NA | | BTCMarkets | No | No| NA |
| BTSE | No | No | Only through website | | BTSE | No | No | Only through website |
| Bybit | Yes | Yes | | | Bybit | Yes | Yes | |
| CoinbasePro | No | No | No| | Coinbase | No | No | No|
| COINUT | No | No | NA | | COINUT | No | No | NA |
| Deribit | Yes | Yes | | | Deribit | Yes | Yes | |
| Exmo | Yes | Yes | Addresses must be created via their website first | | Exmo | Yes | Yes | Addresses must be created via their website first |

View File

@@ -13,4 +13,6 @@ type (
// An UnmarshalTypeError describes a JSON value that was // An UnmarshalTypeError describes a JSON value that was
// not appropriate for a value of a specific Go type. // not appropriate for a value of a specific Go type.
UnmarshalTypeError = json.UnmarshalTypeError UnmarshalTypeError = json.UnmarshalTypeError
// A SyntaxError describes improper JSON
SyntaxError = json.SyntaxError
) )

View File

@@ -1195,11 +1195,11 @@ func (m *DataHistoryManager) validateJob(job *DataHistoryJob) error {
exchangeName := job.Exchange exchangeName := job.Exchange
if job.DataType == dataHistoryCandleValidationSecondarySourceType { if job.DataType == dataHistoryCandleValidationSecondarySourceType {
if job.SecondaryExchangeSource == "" { if job.SecondaryExchangeSource == "" {
return fmt.Errorf("job %s %w, secondary exchange name required to lookup existing results", job.Nickname, errExchangeNameUnset) return fmt.Errorf("job %s %w, secondary exchange name required to lookup existing results", job.Nickname, common.ErrExchangeNameNotSet)
} }
exchangeName = job.SecondaryExchangeSource exchangeName = job.SecondaryExchangeSource
if job.Exchange == "" { if job.Exchange == "" {
return fmt.Errorf("job %s %w, exchange name required to lookup existing results", job.Nickname, errExchangeNameUnset) return fmt.Errorf("job %s %w, exchange name required to lookup existing results", job.Nickname, common.ErrExchangeNameNotSet)
} }
} }
exch, err := m.exchangeManager.GetExchangeByName(exchangeName) exch, err := m.exchangeManager.GetExchangeByName(exchangeName)

View File

@@ -414,12 +414,12 @@ func TestValidateJob(t *testing.T) {
dhj.DataType = dataHistoryCandleValidationSecondarySourceType dhj.DataType = dataHistoryCandleValidationSecondarySourceType
err = m.validateJob(dhj) err = m.validateJob(dhj)
assert.ErrorIs(t, err, errExchangeNameUnset) assert.ErrorIs(t, err, common.ErrExchangeNameNotSet)
dhj.SecondaryExchangeSource = "lol" dhj.SecondaryExchangeSource = "lol"
dhj.Exchange = "" dhj.Exchange = ""
err = m.validateJob(dhj) err = m.validateJob(dhj)
assert.ErrorIs(t, err, errExchangeNameUnset) assert.ErrorIs(t, err, common.ErrExchangeNameNotSet)
} }
func TestGetAllJobStatusBetween(t *testing.T) { func TestGetAllJobStatusBetween(t *testing.T) {

View File

@@ -3,7 +3,6 @@ package engine
import ( import (
"errors" "errors"
"fmt" "fmt"
"maps"
"slices" "slices"
"strings" "strings"
"sync" "sync"
@@ -23,9 +22,12 @@ var (
// DepositAddressManager manages the exchange deposit address store // DepositAddressManager manages the exchange deposit address store
type DepositAddressManager struct { type DepositAddressManager struct {
m sync.RWMutex m sync.RWMutex
store map[string]map[string][]deposit.Address store map[string]ExchangeDepositAddresses
} }
// ExchangeDepositAddresses is a map of currencies to their deposit addresses
type ExchangeDepositAddresses map[string][]deposit.Address
// IsSynced returns whether or not the deposit address store has synced its data // IsSynced returns whether or not the deposit address store has synced its data
func (m *DepositAddressManager) IsSynced() bool { func (m *DepositAddressManager) IsSynced() bool {
if m.store == nil { if m.store == nil {
@@ -39,7 +41,7 @@ func (m *DepositAddressManager) IsSynced() bool {
// SetupDepositAddressManager returns a DepositAddressManager // SetupDepositAddressManager returns a DepositAddressManager
func SetupDepositAddressManager() *DepositAddressManager { func SetupDepositAddressManager() *DepositAddressManager {
return &DepositAddressManager{ return &DepositAddressManager{
store: make(map[string]map[string][]deposit.Address), store: make(map[string]ExchangeDepositAddresses),
} }
} }
@@ -86,7 +88,7 @@ func (m *DepositAddressManager) GetDepositAddressByExchangeAndCurrency(exchName,
// GetDepositAddressesByExchange returns a list of cryptocurrency addresses for the specified // GetDepositAddressesByExchange returns a list of cryptocurrency addresses for the specified
// exchange if they exist // exchange if they exist
func (m *DepositAddressManager) GetDepositAddressesByExchange(exchName string) (map[string][]deposit.Address, error) { func (m *DepositAddressManager) GetDepositAddressesByExchange(exchName string) (ExchangeDepositAddresses, error) {
m.m.RLock() m.m.RLock()
defer m.m.RUnlock() defer m.m.RUnlock()
@@ -99,15 +101,15 @@ func (m *DepositAddressManager) GetDepositAddressesByExchange(exchName string) (
return nil, ErrDepositAddressNotFound return nil, ErrDepositAddressNotFound
} }
cpy := maps.Clone(r) cpy := make(ExchangeDepositAddresses, len(r))
for k, v := range cpy { for k, v := range r {
cpy[k] = slices.Clone(v) cpy[k] = slices.Clone(v)
} }
return cpy, nil return cpy, nil
} }
// Sync synchronises all deposit addresses // Sync synchronises all deposit addresses
func (m *DepositAddressManager) Sync(addresses map[string]map[string][]deposit.Address) error { func (m *DepositAddressManager) Sync(addresses map[string]ExchangeDepositAddresses) error {
if m == nil { if m == nil {
return fmt.Errorf("deposit address manager %w", ErrNilSubsystem) return fmt.Errorf("deposit address manager %w", ErrNilSubsystem)
} }
@@ -118,7 +120,7 @@ func (m *DepositAddressManager) Sync(addresses map[string]map[string][]deposit.A
} }
for k, v := range addresses { for k, v := range addresses {
r := make(map[string][]deposit.Address) r := make(ExchangeDepositAddresses)
for w, x := range v { for w, x := range v {
r[strings.ToUpper(w)] = x r[strings.ToUpper(w)] = x
} }

View File

@@ -21,7 +21,7 @@ func TestIsSynced(t *testing.T) {
t.Error("should be false") t.Error("should be false")
} }
m := SetupDepositAddressManager() m := SetupDepositAddressManager()
err := m.Sync(map[string]map[string][]deposit.Address{ err := m.Sync(map[string]ExchangeDepositAddresses{
bitStamp: { bitStamp: {
btc: []deposit.Address{ btc: []deposit.Address{
{ {
@@ -49,7 +49,7 @@ func TestSetupDepositAddressManager(t *testing.T) {
func TestSync(t *testing.T) { func TestSync(t *testing.T) {
t.Parallel() t.Parallel()
m := SetupDepositAddressManager() m := SetupDepositAddressManager()
err := m.Sync(map[string]map[string][]deposit.Address{ err := m.Sync(map[string]ExchangeDepositAddresses{
bitStamp: { bitStamp: {
btc: []deposit.Address{ btc: []deposit.Address{
{ {
@@ -70,7 +70,7 @@ func TestSync(t *testing.T) {
} }
m.store = nil m.store = nil
err = m.Sync(map[string]map[string][]deposit.Address{ err = m.Sync(map[string]ExchangeDepositAddresses{
bitStamp: { bitStamp: {
btc: []deposit.Address{ btc: []deposit.Address{
{ {
@@ -82,7 +82,7 @@ func TestSync(t *testing.T) {
assert.ErrorIs(t, err, ErrDepositAddressStoreIsNil) assert.ErrorIs(t, err, ErrDepositAddressStoreIsNil)
m = nil m = nil
err = m.Sync(map[string]map[string][]deposit.Address{ err = m.Sync(map[string]ExchangeDepositAddresses{
bitStamp: { bitStamp: {
btc: []deposit.Address{ btc: []deposit.Address{
{ {
@@ -100,7 +100,7 @@ func TestGetDepositAddressByExchangeAndCurrency(t *testing.T) {
_, err := m.GetDepositAddressByExchangeAndCurrency("", "", currency.BTC) _, err := m.GetDepositAddressByExchangeAndCurrency("", "", currency.BTC)
assert.ErrorIs(t, err, ErrDepositAddressStoreIsNil) assert.ErrorIs(t, err, ErrDepositAddressStoreIsNil)
m.store = map[string]map[string][]deposit.Address{ m.store = map[string]ExchangeDepositAddresses{
bitStamp: { bitStamp: {
btc: []deposit.Address{ btc: []deposit.Address{
{ {
@@ -155,7 +155,7 @@ func TestGetDepositAddressesByExchange(t *testing.T) {
_, err := m.GetDepositAddressesByExchange("") _, err := m.GetDepositAddressesByExchange("")
assert.ErrorIs(t, err, ErrDepositAddressStoreIsNil) assert.ErrorIs(t, err, ErrDepositAddressStoreIsNil)
m.store = map[string]map[string][]deposit.Address{ m.store = map[string]ExchangeDepositAddresses{
bitStamp: { bitStamp: {
btc: []deposit.Address{ btc: []deposit.Address{
{ {

View File

@@ -336,8 +336,7 @@ func TestSettingsPrint(t *testing.T) {
} }
var unsupportedDefaultConfigExchanges = []string{ var unsupportedDefaultConfigExchanges = []string{
"poloniex", // poloniex has dropped support for the API GCT has implemented //TODO: drop this when supported "poloniex", // poloniex has dropped support for the API GCT has implemented //TODO: drop this when supported
"coinbasepro", // deprecated API. TODO: Remove this when the Coinbase update is merged
} }
func TestGetDefaultConfigurations(t *testing.T) { func TestGetDefaultConfigurations(t *testing.T) {

View File

@@ -7,6 +7,7 @@ import (
"sync" "sync"
"time" "time"
"github.com/thrasher-corp/gocryptotrader/common"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges" exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/log" "github.com/thrasher-corp/gocryptotrader/log"
) )
@@ -17,7 +18,6 @@ var (
ErrExchangeNotFound = errors.New("exchange not found") ErrExchangeNotFound = errors.New("exchange not found")
ErrExchangeAlreadyLoaded = errors.New("exchange already loaded") ErrExchangeAlreadyLoaded = errors.New("exchange already loaded")
ErrExchangeFailedToLoad = errors.New("exchange failed to load") ErrExchangeFailedToLoad = errors.New("exchange failed to load")
ErrExchangeNameIsEmpty = errors.New("exchange name is empty")
errExchangeIsNil = errors.New("exchange is nil") errExchangeIsNil = errors.New("exchange is nil")
) )
@@ -81,7 +81,7 @@ func (m *ExchangeManager) RemoveExchange(exchangeName string) error {
} }
if exchangeName == "" { if exchangeName == "" {
return fmt.Errorf("exchange manager: %w", ErrExchangeNameIsEmpty) return fmt.Errorf("exchange manager: %w", common.ErrExchangeNameNotSet)
} }
m.mtx.Lock() m.mtx.Lock()
@@ -105,7 +105,7 @@ func (m *ExchangeManager) GetExchangeByName(exchangeName string) (exchange.IBotE
return nil, fmt.Errorf("exchange manager: %w", ErrNilSubsystem) return nil, fmt.Errorf("exchange manager: %w", ErrNilSubsystem)
} }
if exchangeName == "" { if exchangeName == "" {
return nil, fmt.Errorf("exchange manager: %w", ErrExchangeNameIsEmpty) return nil, fmt.Errorf("exchange manager: %w", common.ErrExchangeNameNotSet)
} }
m.mtx.Lock() m.mtx.Lock()
defer m.mtx.Unlock() defer m.mtx.Unlock()

View File

@@ -7,6 +7,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/thrasher-corp/gocryptotrader/common"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges" exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/bitfinex" "github.com/thrasher-corp/gocryptotrader/exchanges/bitfinex"
"github.com/thrasher-corp/gocryptotrader/exchanges/sharedtestvalues" "github.com/thrasher-corp/gocryptotrader/exchanges/sharedtestvalues"
@@ -93,7 +94,7 @@ func TestExchangeManagerRemoveExchange(t *testing.T) {
m = NewExchangeManager() m = NewExchangeManager()
err = m.RemoveExchange("") err = m.RemoveExchange("")
require.ErrorIs(t, err, ErrExchangeNameIsEmpty) require.ErrorIs(t, err, common.ErrExchangeNameNotSet)
err = m.RemoveExchange("Bitfinex") err = m.RemoveExchange("Bitfinex")
require.ErrorIs(t, err, ErrExchangeNotFound) require.ErrorIs(t, err, ErrExchangeNotFound)
@@ -130,7 +131,7 @@ func TestNewExchangeByName(t *testing.T) {
m = NewExchangeManager() m = NewExchangeManager()
_, err = m.NewExchangeByName("") _, err = m.NewExchangeByName("")
require.ErrorIs(t, err, ErrExchangeNameIsEmpty) require.ErrorIs(t, err, common.ErrExchangeNameNotSet)
exchanges := exchange.Exchanges exchanges := exchange.Exchanges
exchanges = append(exchanges, "fake") exchanges = append(exchanges, "fake")

View File

@@ -37,7 +37,7 @@ import (
"github.com/thrasher-corp/gocryptotrader/exchanges/btcmarkets" "github.com/thrasher-corp/gocryptotrader/exchanges/btcmarkets"
"github.com/thrasher-corp/gocryptotrader/exchanges/btse" "github.com/thrasher-corp/gocryptotrader/exchanges/btse"
"github.com/thrasher-corp/gocryptotrader/exchanges/bybit" "github.com/thrasher-corp/gocryptotrader/exchanges/bybit"
"github.com/thrasher-corp/gocryptotrader/exchanges/coinbasepro" "github.com/thrasher-corp/gocryptotrader/exchanges/coinbase"
"github.com/thrasher-corp/gocryptotrader/exchanges/coinut" "github.com/thrasher-corp/gocryptotrader/exchanges/coinut"
"github.com/thrasher-corp/gocryptotrader/exchanges/deposit" "github.com/thrasher-corp/gocryptotrader/exchanges/deposit"
"github.com/thrasher-corp/gocryptotrader/exchanges/deribit" "github.com/thrasher-corp/gocryptotrader/exchanges/deribit"
@@ -694,8 +694,8 @@ func (bot *Engine) GetExchangeCryptocurrencyDepositAddress(ctx context.Context,
} }
// GetAllExchangeCryptocurrencyDepositAddresses obtains an exchanges deposit cryptocurrency list // GetAllExchangeCryptocurrencyDepositAddresses obtains an exchanges deposit cryptocurrency list
func (bot *Engine) GetAllExchangeCryptocurrencyDepositAddresses() map[string]map[string][]deposit.Address { func (bot *Engine) GetAllExchangeCryptocurrencyDepositAddresses() map[string]ExchangeDepositAddresses {
result := make(map[string]map[string][]deposit.Address) result := make(map[string]ExchangeDepositAddresses)
exchanges := bot.GetExchanges() exchanges := bot.GetExchanges()
var depositSyncer sync.WaitGroup var depositSyncer sync.WaitGroup
depositSyncer.Add(len(exchanges)) depositSyncer.Add(len(exchanges))
@@ -992,8 +992,8 @@ func NewSupportedExchangeByName(name string) (exchange.IBotExchange, error) {
return new(deribit.Exchange), nil return new(deribit.Exchange), nil
case "exmo": case "exmo":
return new(exmo.Exchange), nil return new(exmo.Exchange), nil
case "coinbasepro": case "coinbase":
return new(coinbasepro.Exchange), nil return new(coinbase.Exchange), nil
case "gateio": case "gateio":
return new(gateio.Exchange), nil return new(gateio.Exchange), nil
case "gemini": case "gemini":

View File

@@ -349,7 +349,7 @@ func (m *OrderManager) validate(exch exchange.IBotExchange, newOrder *order.Subm
} }
if newOrder.Exchange == "" { if newOrder.Exchange == "" {
return ErrExchangeNameIsEmpty return common.ErrExchangeNameNotSet
} }
if err := newOrder.Validate(exch.GetTradingRequirements()); err != nil { if err := newOrder.Validate(exch.GetTradingRequirements()); err != nil {

View File

@@ -1276,7 +1276,7 @@ func TestSubmitFakeOrder(t *testing.T) {
ord := &order.Submit{} ord := &order.Submit{}
_, err = o.SubmitFakeOrder(ord, resp, false) _, err = o.SubmitFakeOrder(ord, resp, false)
assert.ErrorIs(t, err, ErrExchangeNameIsEmpty) assert.ErrorIs(t, err, common.ErrExchangeNameNotSet)
ord.Exchange = testExchange ord.Exchange = testExchange
ord.AssetType = asset.Spot ord.AssetType = asset.Spot

View File

@@ -64,7 +64,6 @@ var (
errExchangeNotEnabled = errors.New("exchange is not enabled") errExchangeNotEnabled = errors.New("exchange is not enabled")
errExchangeBaseNotFound = errors.New("cannot get exchange base") errExchangeBaseNotFound = errors.New("cannot get exchange base")
errInvalidArguments = errors.New("invalid arguments received") errInvalidArguments = errors.New("invalid arguments received")
errExchangeNameUnset = errors.New("exchange name unset")
errCurrencyPairUnset = errors.New("currency pair unset") errCurrencyPairUnset = errors.New("currency pair unset")
errInvalidTimes = errors.New("invalid start and end times") errInvalidTimes = errors.New("invalid start and end times")
errAssetTypeUnset = errors.New("asset type unset") errAssetTypeUnset = errors.New("asset type unset")
@@ -2102,7 +2101,7 @@ func (s *RPCServer) GetOrderbookStream(r *gctrpc.GetOrderbookStreamRequest, stre
// GetExchangeOrderbookStream streams all orderbooks associated with an exchange // GetExchangeOrderbookStream streams all orderbooks associated with an exchange
func (s *RPCServer) GetExchangeOrderbookStream(r *gctrpc.GetExchangeOrderbookStreamRequest, stream gctrpc.GoCryptoTraderService_GetExchangeOrderbookStreamServer) error { func (s *RPCServer) GetExchangeOrderbookStream(r *gctrpc.GetExchangeOrderbookStreamRequest, stream gctrpc.GoCryptoTraderService_GetExchangeOrderbookStreamServer) error {
if r.Exchange == "" { if r.Exchange == "" {
return errExchangeNameUnset return common.ErrExchangeNameNotSet
} }
if _, err := s.GetExchangeByName(r.Exchange); err != nil { if _, err := s.GetExchangeByName(r.Exchange); err != nil {
@@ -2172,7 +2171,7 @@ func (s *RPCServer) GetExchangeOrderbookStream(r *gctrpc.GetExchangeOrderbookStr
// GetTickerStream streams the requested updated ticker // GetTickerStream streams the requested updated ticker
func (s *RPCServer) GetTickerStream(r *gctrpc.GetTickerStreamRequest, stream gctrpc.GoCryptoTraderService_GetTickerStreamServer) error { func (s *RPCServer) GetTickerStream(r *gctrpc.GetTickerStreamRequest, stream gctrpc.GoCryptoTraderService_GetTickerStreamServer) error {
if r.Exchange == "" { if r.Exchange == "" {
return errExchangeNameUnset return common.ErrExchangeNameNotSet
} }
if _, err := s.GetExchangeByName(r.Exchange); err != nil { if _, err := s.GetExchangeByName(r.Exchange); err != nil {
@@ -2244,7 +2243,7 @@ func (s *RPCServer) GetTickerStream(r *gctrpc.GetTickerStreamRequest, stream gct
// GetExchangeTickerStream streams all tickers associated with an exchange // GetExchangeTickerStream streams all tickers associated with an exchange
func (s *RPCServer) GetExchangeTickerStream(r *gctrpc.GetExchangeTickerStreamRequest, stream gctrpc.GoCryptoTraderService_GetExchangeTickerStreamServer) error { func (s *RPCServer) GetExchangeTickerStream(r *gctrpc.GetExchangeTickerStreamRequest, stream gctrpc.GoCryptoTraderService_GetExchangeTickerStreamServer) error {
if r.Exchange == "" { if r.Exchange == "" {
return errExchangeNameUnset return common.ErrExchangeNameNotSet
} }
if _, err := s.GetExchangeByName(r.Exchange); err != nil { if _, err := s.GetExchangeByName(r.Exchange); err != nil {

View File

@@ -769,7 +769,7 @@ func TestGetHistoricCandles(t *testing.T) {
End: defaultEnd.Format(common.SimpleTimeFormatWithTimezone), End: defaultEnd.Format(common.SimpleTimeFormatWithTimezone),
AssetType: asset.Spot.String(), AssetType: asset.Spot.String(),
}) })
assert.ErrorIs(t, err, ErrExchangeNameIsEmpty) assert.ErrorIs(t, err, common.ErrExchangeNameNotSet)
_, err = s.GetHistoricCandles(t.Context(), &gctrpc.GetHistoricCandlesRequest{ _, err = s.GetHistoricCandles(t.Context(), &gctrpc.GetHistoricCandlesRequest{
Exchange: "bruh", Exchange: "bruh",
@@ -1319,7 +1319,7 @@ func TestGetOrders(t *testing.T) {
AssetType: asset.Spot.String(), AssetType: asset.Spot.String(),
Pair: p, Pair: p,
}) })
assert.ErrorIs(t, err, ErrExchangeNameIsEmpty) assert.ErrorIs(t, err, common.ErrExchangeNameNotSet)
_, err = s.GetOrders(t.Context(), &gctrpc.GetOrdersRequest{ _, err = s.GetOrders(t.Context(), &gctrpc.GetOrdersRequest{
Exchange: "bruh", Exchange: "bruh",
@@ -1838,7 +1838,7 @@ func TestGetManagedOrders(t *testing.T) {
AssetType: asset.Spot.String(), AssetType: asset.Spot.String(),
Pair: p, Pair: p,
}) })
assert.ErrorIs(t, err, ErrExchangeNameIsEmpty) assert.ErrorIs(t, err, common.ErrExchangeNameNotSet)
_, err = s.GetManagedOrders(t.Context(), &gctrpc.GetOrdersRequest{ _, err = s.GetManagedOrders(t.Context(), &gctrpc.GetOrdersRequest{
Exchange: "bruh", Exchange: "bruh",
@@ -2336,7 +2336,7 @@ func TestGetTechnicalAnalysis(t *testing.T) {
} }
_, err = s.GetTechnicalAnalysis(t.Context(), &gctrpc.GetTechnicalAnalysisRequest{}) _, err = s.GetTechnicalAnalysis(t.Context(), &gctrpc.GetTechnicalAnalysisRequest{})
require.ErrorIs(t, err, ErrExchangeNameIsEmpty) require.ErrorIs(t, err, common.ErrExchangeNameNotSet)
_, err = s.GetTechnicalAnalysis(t.Context(), &gctrpc.GetTechnicalAnalysisRequest{ _, err = s.GetTechnicalAnalysis(t.Context(), &gctrpc.GetTechnicalAnalysisRequest{
Exchange: fakeExchangeName, Exchange: fakeExchangeName,
@@ -2572,7 +2572,7 @@ func TestGetMarginRatesHistory(t *testing.T) {
request := &gctrpc.GetMarginRatesHistoryRequest{} request := &gctrpc.GetMarginRatesHistoryRequest{}
_, err = s.GetMarginRatesHistory(t.Context(), request) _, err = s.GetMarginRatesHistory(t.Context(), request)
assert.ErrorIs(t, err, ErrExchangeNameIsEmpty) assert.ErrorIs(t, err, common.ErrExchangeNameNotSet)
request.Exchange = fakeExchangeName request.Exchange = fakeExchangeName
_, err = s.GetMarginRatesHistory(t.Context(), request) _, err = s.GetMarginRatesHistory(t.Context(), request)
@@ -2725,7 +2725,7 @@ func TestGetFundingRates(t *testing.T) {
IncludePayments: false, IncludePayments: false,
} }
_, err = s.GetFundingRates(t.Context(), request) _, err = s.GetFundingRates(t.Context(), request)
assert.ErrorIs(t, err, ErrExchangeNameIsEmpty) assert.ErrorIs(t, err, common.ErrExchangeNameNotSet)
request.Exchange = exch.GetName() request.Exchange = exch.GetName()
_, err = s.GetFundingRates(t.Context(), request) _, err = s.GetFundingRates(t.Context(), request)
@@ -2818,7 +2818,7 @@ func TestGetLatestFundingRate(t *testing.T) {
IncludePredicted: false, IncludePredicted: false,
} }
_, err = s.GetLatestFundingRate(t.Context(), request) _, err = s.GetLatestFundingRate(t.Context(), request)
assert.ErrorIs(t, err, ErrExchangeNameIsEmpty) assert.ErrorIs(t, err, common.ErrExchangeNameNotSet)
request.Exchange = exch.GetName() request.Exchange = exch.GetName()
_, err = s.GetLatestFundingRate(t.Context(), request) _, err = s.GetLatestFundingRate(t.Context(), request)
@@ -2910,7 +2910,7 @@ func TestGetManagedPosition(t *testing.T) {
Quote: "USD", Quote: "USD",
} }
_, err = s.GetManagedPosition(t.Context(), request) _, err = s.GetManagedPosition(t.Context(), request)
assert.ErrorIs(t, err, ErrExchangeNameIsEmpty) assert.ErrorIs(t, err, common.ErrExchangeNameNotSet)
request.Exchange = fakeExchangeName request.Exchange = fakeExchangeName
_, err = s.GetManagedPosition(t.Context(), request) _, err = s.GetManagedPosition(t.Context(), request)
@@ -3096,7 +3096,7 @@ func TestGetOrderbookMovement(t *testing.T) {
req := &gctrpc.GetOrderbookMovementRequest{} req := &gctrpc.GetOrderbookMovementRequest{}
_, err = s.GetOrderbookMovement(t.Context(), req) _, err = s.GetOrderbookMovement(t.Context(), req)
require.ErrorIs(t, err, ErrExchangeNameIsEmpty) require.ErrorIs(t, err, common.ErrExchangeNameNotSet)
req.Exchange = "fake" req.Exchange = "fake"
_, err = s.GetOrderbookMovement(t.Context(), req) _, err = s.GetOrderbookMovement(t.Context(), req)
@@ -3189,7 +3189,7 @@ func TestGetOrderbookAmountByNominal(t *testing.T) {
req := &gctrpc.GetOrderbookAmountByNominalRequest{} req := &gctrpc.GetOrderbookAmountByNominalRequest{}
_, err = s.GetOrderbookAmountByNominal(t.Context(), req) _, err = s.GetOrderbookAmountByNominal(t.Context(), req)
require.ErrorIs(t, err, ErrExchangeNameIsEmpty) require.ErrorIs(t, err, common.ErrExchangeNameNotSet)
req.Exchange = "fake" req.Exchange = "fake"
_, err = s.GetOrderbookAmountByNominal(t.Context(), req) _, err = s.GetOrderbookAmountByNominal(t.Context(), req)
@@ -3275,7 +3275,7 @@ func TestGetOrderbookAmountByImpact(t *testing.T) {
req := &gctrpc.GetOrderbookAmountByImpactRequest{} req := &gctrpc.GetOrderbookAmountByImpactRequest{}
_, err = s.GetOrderbookAmountByImpact(t.Context(), req) _, err = s.GetOrderbookAmountByImpact(t.Context(), req)
require.ErrorIs(t, err, ErrExchangeNameIsEmpty) require.ErrorIs(t, err, common.ErrExchangeNameNotSet)
req.Exchange = "fake" req.Exchange = "fake"
_, err = s.GetOrderbookAmountByImpact(t.Context(), req) _, err = s.GetOrderbookAmountByImpact(t.Context(), req)
@@ -3611,7 +3611,7 @@ func TestSetCollateralMode(t *testing.T) {
req := &gctrpc.SetCollateralModeRequest{} req := &gctrpc.SetCollateralModeRequest{}
_, err = s.SetCollateralMode(t.Context(), req) _, err = s.SetCollateralMode(t.Context(), req)
assert.ErrorIs(t, err, ErrExchangeNameIsEmpty) assert.ErrorIs(t, err, common.ErrExchangeNameNotSet)
req.Exchange = fakeExchangeName req.Exchange = fakeExchangeName
req.Asset = asset.USDTMarginedFutures.String() req.Asset = asset.USDTMarginedFutures.String()
@@ -3648,7 +3648,7 @@ func TestGetCollateralMode(t *testing.T) {
req := &gctrpc.GetCollateralModeRequest{} req := &gctrpc.GetCollateralModeRequest{}
_, err = s.GetCollateralMode(t.Context(), req) _, err = s.GetCollateralMode(t.Context(), req)
assert.ErrorIs(t, err, ErrExchangeNameIsEmpty) assert.ErrorIs(t, err, common.ErrExchangeNameNotSet)
req.Exchange = fakeExchangeName req.Exchange = fakeExchangeName
req.Asset = asset.USDTMarginedFutures.String() req.Asset = asset.USDTMarginedFutures.String()
@@ -3683,7 +3683,7 @@ func TestGetOpenInterest(t *testing.T) {
req := &gctrpc.GetOpenInterestRequest{} req := &gctrpc.GetOpenInterestRequest{}
_, err = s.GetOpenInterest(t.Context(), req) _, err = s.GetOpenInterest(t.Context(), req)
assert.ErrorIs(t, err, ErrExchangeNameIsEmpty) assert.ErrorIs(t, err, common.ErrExchangeNameNotSet)
req.Exchange = fakeExchangeName req.Exchange = fakeExchangeName
_, err = s.GetOpenInterest(t.Context(), req) _, err = s.GetOpenInterest(t.Context(), req)
@@ -3869,7 +3869,7 @@ func TestGetCurrencyTradeURL(t *testing.T) {
req := &gctrpc.GetCurrencyTradeURLRequest{} req := &gctrpc.GetCurrencyTradeURLRequest{}
_, err = s.GetCurrencyTradeURL(t.Context(), req) _, err = s.GetCurrencyTradeURL(t.Context(), req)
assert.ErrorIs(t, err, ErrExchangeNameIsEmpty) assert.ErrorIs(t, err, common.ErrExchangeNameNotSet)
req.Exchange = fakeExchangeName req.Exchange = fakeExchangeName
_, err = s.GetCurrencyTradeURL(t.Context(), req) _, err = s.GetCurrencyTradeURL(t.Context(), req)

View File

@@ -456,7 +456,7 @@ func TestLoadSnapshot(t *testing.T) {
require.ErrorIs(t, err, orderbook.ErrPriceZero) require.ErrorIs(t, err, orderbook.ErrPriceZero)
err = obl.LoadSnapshot(&orderbook.Book{Asks: orderbook.Levels{{Amount: 1}}}) err = obl.LoadSnapshot(&orderbook.Book{Asks: orderbook.Levels{{Amount: 1}}})
require.ErrorIs(t, err, orderbook.ErrExchangeNameEmpty) require.ErrorIs(t, err, common.ErrExchangeNameNotSet)
err = obl.LoadSnapshot(&orderbook.Book{Asks: orderbook.Levels{{Amount: 1}}, Exchange: "test", Pair: cp, Asset: asset.Spot}) err = obl.LoadSnapshot(&orderbook.Book{Asks: orderbook.Levels{{Amount: 1}}, Exchange: "test", Pair: cp, Asset: asset.Spot})
require.ErrorIs(t, err, orderbook.ErrLastUpdatedNotSet) require.ErrorIs(t, err, orderbook.ErrLastUpdatedNotSet)

View File

@@ -23,18 +23,18 @@ import (
// Public websocket errors // Public websocket errors
var ( var (
ErrWebsocketNotEnabled = errors.New("websocket not enabled") ErrWebsocketNotEnabled = errors.New("websocket not enabled")
ErrAlreadyDisabled = errors.New("websocket already disabled") ErrAlreadyDisabled = errors.New("websocket already disabled")
ErrNotConnected = errors.New("websocket is not connected") ErrWebsocketAlreadyEnabled = errors.New("websocket already enabled")
ErrSignatureTimeout = errors.New("websocket timeout waiting for response with signature") ErrNotConnected = errors.New("websocket is not connected")
ErrRequestRouteNotFound = errors.New("request route not found") ErrSignatureTimeout = errors.New("websocket timeout waiting for response with signature")
ErrSignatureNotSet = errors.New("signature not set") ErrRequestRouteNotFound = errors.New("request route not found")
ErrSignatureNotSet = errors.New("signature not set")
) )
// Private websocket errors // Private websocket errors
var ( var (
errWebsocketAlreadyInitialised = errors.New("websocket already initialised") errWebsocketAlreadyInitialised = errors.New("websocket already initialised")
errWebsocketAlreadyEnabled = errors.New("websocket already enabled")
errDefaultURLIsEmpty = errors.New("default url is empty") errDefaultURLIsEmpty = errors.New("default url is empty")
errRunningURLIsEmpty = errors.New("running url cannot be empty") errRunningURLIsEmpty = errors.New("running url cannot be empty")
errInvalidWebsocketURL = errors.New("invalid websocket url") errInvalidWebsocketURL = errors.New("invalid websocket url")
@@ -612,7 +612,7 @@ func (m *Manager) Disable() error {
// Enable enables the exchange websocket protocol // Enable enables the exchange websocket protocol
func (m *Manager) Enable() error { func (m *Manager) Enable() error {
if m.IsConnected() || m.IsEnabled() { if m.IsConnected() || m.IsEnabled() {
return fmt.Errorf("%s %w", m.exchangeName, errWebsocketAlreadyEnabled) return fmt.Errorf("%s %w", m.exchangeName, ErrWebsocketAlreadyEnabled)
} }
m.setEnabled(true) m.setEnabled(true)

View File

@@ -1001,7 +1001,7 @@ func TestEnable(t *testing.T) {
w.Unsubscriber = func(subscription.List) error { return nil } w.Unsubscriber = func(subscription.List) error { return nil }
w.GenerateSubs = func() (subscription.List, error) { return nil, nil } w.GenerateSubs = func() (subscription.List, error) { return nil, nil }
require.NoError(t, w.Enable(), "Enable must not error") require.NoError(t, w.Enable(), "Enable must not error")
assert.ErrorIs(t, w.Enable(), errWebsocketAlreadyEnabled, "Enable should error correctly") assert.ErrorIs(t, w.Enable(), ErrWebsocketAlreadyEnabled, "Enable should error correctly")
} }
func TestSetupNewConnection(t *testing.T) { func TestSetupNewConnection(t *testing.T) {

View File

@@ -25,7 +25,6 @@ var (
var ( var (
errHoldingsIsNil = errors.New("holdings cannot be nil") errHoldingsIsNil = errors.New("holdings cannot be nil")
errExchangeNameUnset = errors.New("exchange name unset")
errNoExchangeSubAccountBalances = errors.New("no exchange sub account balances") errNoExchangeSubAccountBalances = errors.New("no exchange sub account balances")
errBalanceIsNil = errors.New("balance is nil") errBalanceIsNil = errors.New("balance is nil")
errNoCredentialBalances = errors.New("no balances associated with credentials") errNoCredentialBalances = errors.New("no balances associated with credentials")
@@ -107,7 +106,7 @@ func ProcessChange(exch string, changes []Change, c *Credentials) error {
// TODO: Add jurisdiction and differentiation between APIKEY holdings. // TODO: Add jurisdiction and differentiation between APIKEY holdings.
func GetHoldings(exch string, creds *Credentials, assetType asset.Item) (Holdings, error) { func GetHoldings(exch string, creds *Credentials, assetType asset.Item) (Holdings, error) {
if exch == "" { if exch == "" {
return Holdings{}, errExchangeNameUnset return Holdings{}, common.ErrExchangeNameNotSet
} }
if creds.IsEmpty() { if creds.IsEmpty() {
@@ -171,7 +170,7 @@ func GetHoldings(exch string, creds *Credentials, assetType asset.Item) (Holding
// GetBalance returns the internal balance for that asset item. // GetBalance returns the internal balance for that asset item.
func GetBalance(exch, subAccount string, creds *Credentials, a asset.Item, c currency.Code) (*ProtectedBalance, error) { func GetBalance(exch, subAccount string, creds *Credentials, a asset.Item, c currency.Code) (*ProtectedBalance, error) {
if exch == "" { if exch == "" {
return nil, fmt.Errorf("cannot get balance: %w", errExchangeNameUnset) return nil, fmt.Errorf("cannot get balance: %w", common.ErrExchangeNameNotSet)
} }
if !a.IsValid() { if !a.IsValid() {
@@ -222,7 +221,7 @@ func (s *Service) Save(incoming *Holdings, creds *Credentials) error {
} }
if incoming.Exchange == "" { if incoming.Exchange == "" {
return fmt.Errorf("cannot save holdings: %w", errExchangeNameUnset) return fmt.Errorf("cannot save holdings: %w", common.ErrExchangeNameNotSet)
} }
if creds.IsEmpty() { if creds.IsEmpty() {
@@ -316,7 +315,7 @@ func (s *Service) Save(incoming *Holdings, creds *Credentials) error {
// Update updates the balance for a specific exchange and credentials // Update updates the balance for a specific exchange and credentials
func (s *Service) Update(exch string, changes []Change, creds *Credentials) error { func (s *Service) Update(exch string, changes []Change, creds *Credentials) error {
if exch == "" { if exch == "" {
return fmt.Errorf("%w: %w", errCannotUpdateBalance, errExchangeNameUnset) return fmt.Errorf("%w: %w", errCannotUpdateBalance, common.ErrExchangeNameNotSet)
} }
if creds.IsEmpty() { if creds.IsEmpty() {

View File

@@ -68,7 +68,7 @@ func TestGetHoldings(t *testing.T) {
assert.ErrorIs(t, err, errHoldingsIsNil) assert.ErrorIs(t, err, errHoldingsIsNil)
err = Process(&Holdings{}, nil) err = Process(&Holdings{}, nil)
assert.ErrorIs(t, err, errExchangeNameUnset) assert.ErrorIs(t, err, common.ErrExchangeNameNotSet)
holdings := Holdings{Exchange: "Test"} holdings := Holdings{Exchange: "Test"}
@@ -111,7 +111,7 @@ func TestGetHoldings(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
_, err = GetHoldings("", nil, asset.Spot) _, err = GetHoldings("", nil, asset.Spot)
assert.ErrorIs(t, err, errExchangeNameUnset) assert.ErrorIs(t, err, common.ErrExchangeNameNotSet)
_, err = GetHoldings("bla", nil, asset.Spot) _, err = GetHoldings("bla", nil, asset.Spot)
assert.ErrorIs(t, err, errCredentialsAreNil) assert.ErrorIs(t, err, errCredentialsAreNil)
@@ -182,7 +182,7 @@ func TestGetBalance(t *testing.T) {
t.Parallel() t.Parallel()
_, err := GetBalance("", "", nil, asset.Empty, currency.Code{}) _, err := GetBalance("", "", nil, asset.Empty, currency.Code{})
assert.ErrorIs(t, err, errExchangeNameUnset) assert.ErrorIs(t, err, common.ErrExchangeNameNotSet)
_, err = GetBalance("bruh", "", nil, asset.Empty, currency.Code{}) _, err = GetBalance("bruh", "", nil, asset.Empty, currency.Code{})
assert.ErrorIs(t, err, asset.ErrNotSupported) assert.ErrorIs(t, err, asset.ErrNotSupported)
@@ -320,7 +320,7 @@ func TestSave(t *testing.T) {
assert.ErrorIs(t, err, errHoldingsIsNil) assert.ErrorIs(t, err, errHoldingsIsNil)
err = s.Save(&Holdings{}, nil) err = s.Save(&Holdings{}, nil)
assert.ErrorIs(t, err, errExchangeNameUnset) assert.ErrorIs(t, err, common.ErrExchangeNameNotSet)
err = s.Save(&Holdings{ err = s.Save(&Holdings{
Exchange: "TeSt", Exchange: "TeSt",
@@ -410,7 +410,7 @@ func TestUpdate(t *testing.T) {
t.Parallel() t.Parallel()
s := &Service{exchangeAccounts: make(map[string]*Accounts), mux: dispatch.GetNewMux(nil)} s := &Service{exchangeAccounts: make(map[string]*Accounts), mux: dispatch.GetNewMux(nil)}
err := s.Update("", nil, nil) err := s.Update("", nil, nil)
assert.ErrorIs(t, err, errExchangeNameUnset) assert.ErrorIs(t, err, common.ErrExchangeNameNotSet)
err = s.Update("test", nil, nil) err = s.Update("test", nil, nil)
assert.ErrorIs(t, err, errCredentialsAreNil) assert.ErrorIs(t, err, errCredentialsAreNil)

View File

@@ -133,6 +133,11 @@ func (a Item) String() string {
} }
} }
// Upper returns the item's upper case string
func (a Item) Upper() string {
return strings.ToUpper(a.String())
}
// Strings converts an asset type array to a string array // Strings converts an asset type array to a string array
func (a Items) Strings() []string { func (a Items) Strings() []string {
assets := make([]string, len(a)) assets := make([]string, len(a))

View File

@@ -20,6 +20,14 @@ func TestString(t *testing.T) {
} }
} }
func TestUpper(t *testing.T) {
t.Parallel()
a := Spot
require.Equal(t, "SPOT", a.Upper())
a = 0
require.Empty(t, a.Upper())
}
func TestStrings(t *testing.T) { func TestStrings(t *testing.T) {
t.Parallel() t.Parallel()
assert.ElementsMatch(t, Items{Spot, Futures}.Strings(), []string{"spot", "futures"}) assert.ElementsMatch(t, Items{Spot, Futures}.Strings(), []string{"spot", "futures"})

View File

@@ -934,7 +934,7 @@ func TestWsTrades(t *testing.T) {
require.NoError(t, e.wsHandleData(msg), "Must not error handling a standard stream of trades") require.NoError(t, e.wsHandleData(msg), "Must not error handling a standard stream of trades")
msg = []byte(`{"table":"trade","action":"insert","data":[{"timestamp":"2020-02-17T01:35:36.442Z","symbol":".BGCT","size":14,"price":258.2,"side":"sell"}]}`) msg = []byte(`{"table":"trade","action":"insert","data":[{"timestamp":"2020-02-17T01:35:36.442Z","symbol":".BGCT","size":14,"price":258.2,"side":"sell"}]}`)
require.ErrorIs(t, e.wsHandleData(msg), exchange.ErrSymbolCannotBeMatched, "Must error correctly with an unknown symbol") require.ErrorIs(t, e.wsHandleData(msg), exchange.ErrSymbolNotMatched, "Must error correctly with an unknown symbol")
msg = []byte(`{"table":"trade","action":"insert","data":[{"timestamp":"2020-02-17T01:35:36.442Z","symbol":".BGCT","size":0,"price":258.2,"side":"sell"}]}`) msg = []byte(`{"table":"trade","action":"insert","data":[{"timestamp":"2020-02-17T01:35:36.442Z","symbol":".BGCT","size":0,"price":258.2,"side":"sell"}]}`)
require.NoError(t, e.wsHandleData(msg), "Must not error that symbol is unknown when index trade is ignored due to zero size") require.NoError(t, e.wsHandleData(msg), "Must not error that symbol is unknown when index trade is ignored due to zero size")

View File

@@ -1,16 +1,16 @@
# GoCryptoTrader package Coinbasepro # GoCryptoTrader package Coinbase
<img src="/common/gctlogo.png?raw=true" width="350px" height="350px" hspace="70"> <img src="/common/gctlogo.png?raw=true" width="350px" height="350px" hspace="70">
[![Build Status](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml) [![Build Status](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml)
[![Software License](https://img.shields.io/badge/License-MIT-orange.svg?style=flat-square)](https://github.com/thrasher-corp/gocryptotrader/blob/master/LICENSE) [![Software License](https://img.shields.io/badge/License-MIT-orange.svg?style=flat-square)](https://github.com/thrasher-corp/gocryptotrader/blob/master/LICENSE)
[![GoDoc](https://godoc.org/github.com/thrasher-corp/gocryptotrader?status.svg)](https://godoc.org/github.com/thrasher-corp/gocryptotrader/exchanges/coinbasepro) [![GoDoc](https://godoc.org/github.com/thrasher-corp/gocryptotrader?status.svg)](https://godoc.org/github.com/thrasher-corp/gocryptotrader/exchanges/Coinbase)
[![Coverage Status](https://codecov.io/gh/thrasher-corp/gocryptotrader/graph/badge.svg?token=41784B23TS)](https://codecov.io/gh/thrasher-corp/gocryptotrader) [![Coverage Status](https://codecov.io/gh/thrasher-corp/gocryptotrader/graph/badge.svg?token=41784B23TS)](https://codecov.io/gh/thrasher-corp/gocryptotrader)
[![Go Report Card](https://goreportcard.com/badge/github.com/thrasher-corp/gocryptotrader)](https://goreportcard.com/report/github.com/thrasher-corp/gocryptotrader) [![Go Report Card](https://goreportcard.com/badge/github.com/thrasher-corp/gocryptotrader)](https://goreportcard.com/report/github.com/thrasher-corp/gocryptotrader)
This coinbasepro package is part of the GoCryptoTrader codebase. This coinbase package is part of the GoCryptoTrader codebase.
## This is still in active development ## This is still in active development
@@ -18,7 +18,7 @@ You can track ideas, planned features and what's in progress on our [GoCryptoTra
Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader Slack](https://join.slack.com/t/gocryptotrader/shared_invite/zt-38z8abs3l-gH8AAOk8XND6DP5NfCiG_g) Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader Slack](https://join.slack.com/t/gocryptotrader/shared_invite/zt-38z8abs3l-gH8AAOk8XND6DP5NfCiG_g)
## CoinbasePro Exchange ## Coinbase Exchange
### Current Features ### Current Features
@@ -49,7 +49,7 @@ main.go
var c exchange.IBotExchange var c exchange.IBotExchange
for i := range bot.Exchanges { for i := range bot.Exchanges {
if bot.Exchanges[i].GetName() == "CoinbasePro" { if bot.Exchanges[i].GetName() == "Coinbase" {
c = bot.Exchanges[i] c = bot.Exchanges[i]
} }
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View 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 }}
`

File diff suppressed because it is too large Load Diff

View 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),
}

View File

@@ -1,810 +0,0 @@
package coinbasepro
import (
"bytes"
"context"
"encoding/base64"
"errors"
"fmt"
"net/http"
"net/url"
"slices"
"strconv"
"strings"
"time"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/common/crypto"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/encoding/json"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
)
const (
coinbaseproAPIURL = "https://api.pro.coinbase.com/"
coinbaseproSandboxAPIURL = "https://api-public.sandbox.pro.coinbase.com/"
tradeBaseURL = "https://www.coinbase.com/advanced-trade/spot/"
coinbaseproAPIVersion = "0"
coinbaseproProducts = "products"
coinbaseproOrderbook = "book"
coinbaseproTicker = "ticker"
coinbaseproTrades = "trades"
coinbaseproHistory = "candles"
coinbaseproStats = "stats"
coinbaseproCurrencies = "currencies"
coinbaseproAccounts = "accounts"
coinbaseproLedger = "ledger"
coinbaseproHolds = "holds"
coinbaseproOrders = "orders"
coinbaseproFills = "fills"
coinbaseproTransfers = "transfers"
coinbaseproReports = "reports"
coinbaseproTime = "time"
coinbaseproMarginTransfer = "profiles/margin-transfer"
coinbaseproPosition = "position"
coinbaseproPositionClose = "position/close"
coinbaseproPaymentMethod = "payment-methods"
coinbaseproPaymentMethodDeposit = "deposits/payment-method"
coinbaseproDepositCoinbase = "deposits/coinbase-account"
coinbaseproWithdrawalPaymentMethod = "withdrawals/payment-method"
coinbaseproWithdrawalCoinbase = "withdrawals/coinbase"
coinbaseproWithdrawalCrypto = "withdrawals/crypto"
coinbaseproCoinbaseAccounts = "coinbase-accounts"
coinbaseproTrailingVolume = "users/self/trailing-volume"
)
// Exchange implements exchange.IBotExchange and contains additional specific api methods for interacting with CoinbasePro
type Exchange struct {
exchange.Base
}
// GetProducts returns supported currency pairs on the exchange with specific
// information about the pair
func (e *Exchange) GetProducts(ctx context.Context) ([]Product, error) {
var products []Product
return products, e.SendHTTPRequest(ctx, exchange.RestSpot, coinbaseproProducts, &products)
}
// GetOrderbook returns orderbook by currency pair and level
func (e *Exchange) GetOrderbook(ctx context.Context, symbol string, level int) (any, error) {
orderbook := OrderbookResponse{}
path := fmt.Sprintf("%s/%s/%s", coinbaseproProducts, symbol, coinbaseproOrderbook)
if level > 0 {
levelStr := strconv.Itoa(level)
path = fmt.Sprintf("%s/%s/%s?level=%s", coinbaseproProducts, symbol, coinbaseproOrderbook, levelStr)
}
if err := e.SendHTTPRequest(ctx, exchange.RestSpot, path, &orderbook); err != nil {
return nil, err
}
if level == 3 {
ob := OrderbookL3{
Sequence: orderbook.Sequence,
Bids: make([]OrderL3, len(orderbook.Bids)),
Asks: make([]OrderL3, len(orderbook.Asks)),
}
ob.Sequence = orderbook.Sequence
for x := range orderbook.Asks {
ob.Asks[x].Price = orderbook.Asks[x][0].Float64()
ob.Asks[x].Amount = orderbook.Asks[x][1].Float64()
ob.Asks[x].OrderID = orderbook.Asks[x][2].String()
}
for x := range orderbook.Bids {
ob.Bids[x].Price = orderbook.Bids[x][0].Float64()
ob.Bids[x].Amount = orderbook.Bids[x][1].Float64()
ob.Bids[x].OrderID = orderbook.Bids[x][2].String()
}
return ob, nil
}
ob := OrderbookL1L2{
Sequence: orderbook.Sequence,
Bids: make([]OrderL1L2, len(orderbook.Bids)),
Asks: make([]OrderL1L2, len(orderbook.Asks)),
}
for x := range orderbook.Asks {
ob.Asks[x].Price = orderbook.Asks[x][0].Float64()
ob.Asks[x].Amount = orderbook.Asks[x][1].Float64()
ob.Asks[x].NumOrders = orderbook.Asks[x][2].Float64()
}
for x := range orderbook.Bids {
ob.Bids[x].Price = orderbook.Bids[x][0].Float64()
ob.Bids[x].Amount = orderbook.Bids[x][1].Float64()
ob.Bids[x].NumOrders = orderbook.Bids[x][2].Float64()
}
return ob, nil
}
// GetTicker returns ticker by currency pair
// currencyPair - example "BTC-USD"
func (e *Exchange) GetTicker(ctx context.Context, currencyPair string) (Ticker, error) {
tick := Ticker{}
path := fmt.Sprintf(
"%s/%s/%s", coinbaseproProducts, currencyPair, coinbaseproTicker)
return tick, e.SendHTTPRequest(ctx, exchange.RestSpot, path, &tick)
}
// GetTrades listd the latest trades for a product
// currencyPair - example "BTC-USD"
func (e *Exchange) GetTrades(ctx context.Context, currencyPair string) ([]Trade, error) {
var trades []Trade
path := fmt.Sprintf(
"%s/%s/%s", coinbaseproProducts, currencyPair, coinbaseproTrades)
return trades, e.SendHTTPRequest(ctx, exchange.RestSpot, path, &trades)
}
// GetHistoricRates returns historic rates for a product. Rates are returned in
// grouped buckets based on requested granularity.
func (e *Exchange) GetHistoricRates(ctx context.Context, currencyPair, start, end string, granularity int64) ([]History, error) {
values := url.Values{}
if start != "" {
values.Set("start", start)
} else {
values.Set("start", "")
}
if end != "" {
values.Set("end", end)
} else {
values.Set("end", "")
}
allowedGranularities := []int64{60, 300, 900, 3600, 21600, 86400}
if !slices.Contains(allowedGranularities, granularity) {
return nil, errors.New("Invalid granularity value: " + strconv.FormatInt(granularity, 10) + ". Allowed values are {60, 300, 900, 3600, 21600, 86400}")
}
if granularity > 0 {
values.Set("granularity", strconv.FormatInt(granularity, 10))
}
var resp []History
path := common.EncodeURLValues(
fmt.Sprintf("%s/%s/%s", coinbaseproProducts, currencyPair, coinbaseproHistory),
values)
return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, path, &resp)
}
// GetStats returns a 24 hr stat for the product. Volume is in base currency
// units. open, high, low are in quote currency units.
func (e *Exchange) GetStats(ctx context.Context, currencyPair string) (Stats, error) {
stats := Stats{}
path := fmt.Sprintf(
"%s/%s/%s", coinbaseproProducts, currencyPair, coinbaseproStats)
return stats, e.SendHTTPRequest(ctx, exchange.RestSpot, path, &stats)
}
// GetCurrencies returns a list of supported currency on the exchange
// Warning: Not all currencies may be currently in use for tradinc.
func (e *Exchange) GetCurrencies(ctx context.Context) ([]Currency, error) {
var currencies []Currency
return currencies, e.SendHTTPRequest(ctx, exchange.RestSpot, coinbaseproCurrencies, &currencies)
}
// GetCurrentServerTime returns the API server time
func (e *Exchange) GetCurrentServerTime(ctx context.Context) (ServerTime, error) {
serverTime := ServerTime{}
return serverTime, e.SendHTTPRequest(ctx, exchange.RestSpot, coinbaseproTime, &serverTime)
}
// GetAccounts returns a list of trading accounts associated with the APIKEYS
func (e *Exchange) GetAccounts(ctx context.Context) ([]AccountResponse, error) {
var resp []AccountResponse
return resp,
e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, coinbaseproAccounts, nil, &resp)
}
// GetAccount returns information for a single account. Use this endpoint when
// account_id is known
func (e *Exchange) GetAccount(ctx context.Context, accountID string) (AccountResponse, error) {
resp := AccountResponse{}
path := fmt.Sprintf("%s/%s", coinbaseproAccounts, accountID)
return resp, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, nil, &resp)
}
// GetAccountHistory returns a list of account activity. Account activity either
// increases or decreases your account balance. Items are paginated and sorted
// latest first.
func (e *Exchange) GetAccountHistory(ctx context.Context, accountID string) ([]AccountLedgerResponse, error) {
var resp []AccountLedgerResponse
path := fmt.Sprintf("%s/%s/%s", coinbaseproAccounts, accountID, coinbaseproLedger)
return resp, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, nil, &resp)
}
// GetHolds returns the holds that are placed on an account for any active
// orders or pending withdraw requests. As an order is filled, the hold amount
// is updated. If an order is canceled, any remaining hold is removed. For a
// withdraw, once it is completed, the hold is removed.
func (e *Exchange) GetHolds(ctx context.Context, accountID string) ([]AccountHolds, error) {
var resp []AccountHolds
path := fmt.Sprintf("%s/%s/%s", coinbaseproAccounts, accountID, coinbaseproHolds)
return resp, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, nil, &resp)
}
// PlaceLimitOrder places a new limit order. Orders can only be placed if the
// account has sufficient funds. Once an order is placed, account funds
// will be put on hold for the duration of the order. How much and which funds
// are put on hold depends on the order type and parameters specified.
//
// GENERAL PARAMS
// clientRef - [optional] Order ID selected by you to identify your order
// side - buy or sell
// productID - A valid product id
// stp - [optional] Self-trade prevention flag
//
// LIMIT ORDER PARAMS
// price - Price per bitcoin
// amount - Amount of BTC to buy or sell
// timeInforce - [optional] GTC, GTT, IOC, or FOK (default is GTC)
// cancelAfter - [optional] min, hour, day * Requires time_in_force to be GTT
// postOnly - [optional] Post only flag Invalid when time_in_force is IOC or FOK
func (e *Exchange) PlaceLimitOrder(ctx context.Context, clientRef string, price, amount float64, side, timeInforce, cancelAfter, productID, stp string, postOnly bool) (string, error) {
req := make(map[string]any)
req["type"] = order.Limit.Lower()
req["price"] = strconv.FormatFloat(price, 'f', -1, 64)
req["size"] = strconv.FormatFloat(amount, 'f', -1, 64)
req["side"] = side
req["product_id"] = productID
if cancelAfter != "" {
req["cancel_after"] = cancelAfter
}
if timeInforce != "" {
req["time_in_force"] = timeInforce
}
if clientRef != "" {
req["client_oid"] = clientRef
}
if stp != "" {
req["stp"] = stp
}
if postOnly {
req["post_only"] = postOnly
}
resp := GeneralizedOrderResponse{}
return resp.ID, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, coinbaseproOrders, req, &resp)
}
// PlaceMarketOrder places a new market order.
// Orders can only be placed if the account has sufficient funds. Once an order
// is placed, account funds will be put on hold for the duration of the order.
// How much and which funds are put on hold depends on the order type and
// parameters specified.
//
// GENERAL PARAMS
// clientRef - [optional] Order ID selected by you to identify your order
// side - buy or sell
// productID - A valid product id
// stp - [optional] Self-trade prevention flag
//
// MARKET ORDER PARAMS
// size - [optional]* Desired amount in BTC
// funds [optional]* Desired amount of quote currency to use
// * One of size or funds is required.
func (e *Exchange) PlaceMarketOrder(ctx context.Context, clientRef string, size, funds float64, side, productID, stp string) (string, error) {
resp := GeneralizedOrderResponse{}
req := make(map[string]any)
req["side"] = side
req["product_id"] = productID
req["type"] = order.Market.Lower()
if size != 0 {
req["size"] = strconv.FormatFloat(size, 'f', -1, 64)
}
if funds != 0 {
req["funds"] = strconv.FormatFloat(funds, 'f', -1, 64)
}
if clientRef != "" {
req["client_oid"] = clientRef
}
if stp != "" {
req["stp"] = stp
}
err := e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, coinbaseproOrders, req, &resp)
if err != nil {
return "", err
}
return resp.ID, nil
}
// PlaceMarginOrder places a new market order.
// Orders can only be placed if the account has sufficient funds. Once an order
// is placed, account funds will be put on hold for the duration of the order.
// How much and which funds are put on hold depends on the order type and
// parameters specified.
//
// GENERAL PARAMS
// clientRef - [optional] Order ID selected by you to identify your order
// side - buy or sell
// productID - A valid product id
// stp - [optional] Self-trade prevention flag
//
// MARGIN ORDER PARAMS
// size - [optional]* Desired amount in BTC
// funds - [optional]* Desired amount of quote currency to use
func (e *Exchange) PlaceMarginOrder(ctx context.Context, clientRef string, size, funds float64, side, productID, stp string) (string, error) {
resp := GeneralizedOrderResponse{}
req := make(map[string]any)
req["side"] = side
req["product_id"] = productID
req["type"] = "margin"
if size != 0 {
req["size"] = strconv.FormatFloat(size, 'f', -1, 64)
}
if funds != 0 {
req["funds"] = strconv.FormatFloat(funds, 'f', -1, 64)
}
if clientRef != "" {
req["client_oid"] = clientRef
}
if stp != "" {
req["stp"] = stp
}
err := e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, coinbaseproOrders, req, &resp)
if err != nil {
return "", err
}
return resp.ID, nil
}
// CancelExistingOrder cancels order by orderID
func (e *Exchange) CancelExistingOrder(ctx context.Context, orderID string) error {
path := fmt.Sprintf("%s/%s", coinbaseproOrders, orderID)
return e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodDelete, path, nil, nil)
}
// CancelAllExistingOrders cancels all open orders on the exchange and returns
// and array of order IDs
// currencyPair - [optional] all orders for a currencyPair string will be
// canceled
func (e *Exchange) CancelAllExistingOrders(ctx context.Context, currencyPair string) ([]string, error) {
var resp []string
req := make(map[string]any)
if currencyPair != "" {
req["product_id"] = currencyPair
}
return resp, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodDelete, coinbaseproOrders, req, &resp)
}
// GetOrders lists current open orders. Only open or un-settled orders are
// returned. As soon as an order is no longer open and settled, it will no
// longer appear in the default request.
// status - can be a range of "open", "pending", "done" or "active"
// currencyPair - [optional] for example "BTC-USD"
func (e *Exchange) GetOrders(ctx context.Context, status []string, currencyPair string) ([]GeneralizedOrderResponse, error) {
var resp []GeneralizedOrderResponse
params := url.Values{}
for _, individualStatus := range status {
params.Add("status", individualStatus)
}
if currencyPair != "" {
params.Set("product_id", currencyPair)
}
path := common.EncodeURLValues(coinbaseproOrders, params)
return resp,
e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, nil, &resp)
}
// GetOrder returns a single order by order id.
func (e *Exchange) GetOrder(ctx context.Context, orderID string) (GeneralizedOrderResponse, error) {
resp := GeneralizedOrderResponse{}
path := fmt.Sprintf("%s/%s", coinbaseproOrders, orderID)
return resp, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, nil, &resp)
}
// GetFills returns a list of recent fills
func (e *Exchange) GetFills(ctx context.Context, orderID, currencyPair string) ([]FillResponse, error) {
var resp []FillResponse
params := url.Values{}
if orderID != "" {
params.Set("order_id", orderID)
}
if currencyPair != "" {
params.Set("product_id", currencyPair)
}
if params.Get("order_id") == "" && params.Get("product_id") == "" {
return resp, errors.New("no parameters set")
}
path := common.EncodeURLValues(coinbaseproFills, params)
return resp,
e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, nil, &resp)
}
// MarginTransfer sends funds between a standard/default profile and a margin
// profile.
// A deposit will transfer funds from the default profile into the margin
// profile. A withdraw will transfer funds from the margin profile to the
// default profile. Withdraws will fail if they would set your margin ratio
// below the initial margin ratio requirement.
//
// amount - the amount to transfer between the default and margin profile
// transferType - either "deposit" or "withdraw"
// profileID - The id of the margin profile to deposit or withdraw from
// currency - currency to transfer, currently on "BTC" or "USD"
func (e *Exchange) MarginTransfer(ctx context.Context, amount float64, transferType, profileID, ccy string) (MarginTransfer, error) {
resp := MarginTransfer{}
req := make(map[string]any)
req["type"] = transferType
req["amount"] = strconv.FormatFloat(amount, 'f', -1, 64)
req["currency"] = ccy
req["margin_profile_id"] = profileID
return resp,
e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, coinbaseproMarginTransfer, req, &resp)
}
// GetPosition returns an overview of account profile.
func (e *Exchange) GetPosition(ctx context.Context) (AccountOverview, error) {
resp := AccountOverview{}
return resp,
e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, coinbaseproPosition, nil, &resp)
}
// ClosePosition closes a position and allowing you to repay position as well
// repayOnly - allows the position to be repaid
func (e *Exchange) ClosePosition(ctx context.Context, repayOnly bool) (AccountOverview, error) {
resp := AccountOverview{}
req := make(map[string]any)
req["repay_only"] = repayOnly
return resp,
e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, coinbaseproPositionClose, req, &resp)
}
// GetPayMethods returns a full list of payment methods
func (e *Exchange) GetPayMethods(ctx context.Context) ([]PaymentMethod, error) {
var resp []PaymentMethod
return resp,
e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, coinbaseproPaymentMethod, nil, &resp)
}
// DepositViaPaymentMethod deposits funds from a payment method. See the Payment
// Methods section for retrieving your payment methods.
//
// amount - The amount to deposit
// currency - The type of currency
// paymentID - ID of the payment method
func (e *Exchange) DepositViaPaymentMethod(ctx context.Context, amount float64, ccy, paymentID string) (DepositWithdrawalInfo, error) {
resp := DepositWithdrawalInfo{}
req := make(map[string]any)
req["amount"] = amount
req["currency"] = ccy
req["payment_method_id"] = paymentID
return resp,
e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, coinbaseproPaymentMethodDeposit, req, &resp)
}
// DepositViaCoinbase deposits funds from a coinbase account. Move funds between
// a Coinbase account and coinbasepro trading account within daily limits. Moving
// funds between Coinbase and coinbasepro is instant and free. See the Coinbase
// Accounts section for retrieving your Coinbase accounts.
//
// amount - The amount to deposit
// currency - The type of currency
// accountID - ID of the coinbase account
func (e *Exchange) DepositViaCoinbase(ctx context.Context, amount float64, ccy, accountID string) (DepositWithdrawalInfo, error) {
resp := DepositWithdrawalInfo{}
req := make(map[string]any)
req["amount"] = amount
req["currency"] = ccy
req["coinbase_account_id"] = accountID
return resp,
e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, coinbaseproDepositCoinbase, req, &resp)
}
// WithdrawViaPaymentMethod withdraws funds to a payment method
//
// amount - The amount to withdraw
// currency - The type of currency
// paymentID - ID of the payment method
func (e *Exchange) WithdrawViaPaymentMethod(ctx context.Context, amount float64, ccy, paymentID string) (DepositWithdrawalInfo, error) {
resp := DepositWithdrawalInfo{}
req := make(map[string]any)
req["amount"] = amount
req["currency"] = ccy
req["payment_method_id"] = paymentID
return resp,
e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, coinbaseproWithdrawalPaymentMethod, req, &resp)
}
// /////////////////////// NO ROUTE FOUND ERROR ////////////////////////////////
// WithdrawViaCoinbase withdraws funds to a coinbase account.
//
// amount - The amount to withdraw
// currency - The type of currency
// accountID - ID of the coinbase account
// func (c *CoinbasePro) WithdrawViaCoinbase(amount float64, currency, accountID string) (DepositWithdrawalInfo, error) {
// resp := DepositWithdrawalInfo{}
// req := make(map[string]any)
// req["amount"] = amount
// req["currency"] = currency
// req["coinbase_account_id"] = accountID
//
// return resp,
// c.SendAuthenticatedHTTPRequest(ctx,http.MethodPost, coinbaseproWithdrawalCoinbase, req, &resp)
// }
// WithdrawCrypto withdraws funds to a crypto address
//
// amount - The amount to withdraw
// currency - The type of currency
// cryptoAddress - A crypto address of the recipient
func (e *Exchange) WithdrawCrypto(ctx context.Context, amount float64, ccy, cryptoAddress string) (DepositWithdrawalInfo, error) {
resp := DepositWithdrawalInfo{}
req := make(map[string]any)
req["amount"] = amount
req["currency"] = ccy
req["crypto_address"] = cryptoAddress
return resp,
e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, coinbaseproWithdrawalCrypto, req, &resp)
}
// GetCoinbaseAccounts returns a list of coinbase accounts
func (e *Exchange) GetCoinbaseAccounts(ctx context.Context) ([]CoinbaseAccounts, error) {
var resp []CoinbaseAccounts
return resp,
e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, coinbaseproCoinbaseAccounts, nil, &resp)
}
// GetReport returns batches of historic information about your account in
// various human and machine readable forms.
//
// reportType - "fills" or "account"
// startDate - Starting date for the report (inclusive)
// endDate - Ending date for the report (inclusive)
// currencyPair - ID of the product to generate a fills report for.
// E.c. BTC-USD. *Required* if type is fills
// accountID - ID of the account to generate an account report for. *Required*
// if type is account
// format - pdf or csv (default is pdf)
// email - [optional] Email address to send the report to
func (e *Exchange) GetReport(ctx context.Context, reportType, startDate, endDate, currencyPair, accountID, format, email string) (Report, error) {
resp := Report{}
req := make(map[string]any)
req["type"] = reportType
req["start_date"] = startDate
req["end_date"] = endDate
req["format"] = "pdf"
if currencyPair != "" {
req["product_id"] = currencyPair
}
if accountID != "" {
req["account_id"] = accountID
}
if format == "csv" {
req["format"] = format
}
if email != "" {
req["email"] = email
}
return resp,
e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, coinbaseproReports, req, &resp)
}
// GetReportStatus once a report request has been accepted for processing, the
// status is available by polling the report resource endpoint.
func (e *Exchange) GetReportStatus(ctx context.Context, reportID string) (Report, error) {
resp := Report{}
path := fmt.Sprintf("%s/%s", coinbaseproReports, reportID)
return resp, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, nil, &resp)
}
// GetTrailingVolume this request will return your 30-day trailing volume for
// all products.
func (e *Exchange) GetTrailingVolume(ctx context.Context) ([]Volume, error) {
var resp []Volume
return resp,
e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, coinbaseproTrailingVolume, nil, &resp)
}
// GetTransfers returns a history of withdrawal and or deposit transactions
func (e *Exchange) GetTransfers(ctx context.Context, profileID, transferType string, limit int64, start, end time.Time) ([]TransferHistory, error) {
if !start.IsZero() && !end.IsZero() {
err := common.StartEndTimeCheck(start, end)
if err != nil {
return nil, err
}
}
req := make(map[string]any)
if profileID != "" {
req["profile_id"] = profileID
}
if !start.IsZero() {
req["before"] = start.Format(time.RFC3339)
}
if !end.IsZero() {
req["after"] = end.Format(time.RFC3339)
}
if limit > 0 {
req["limit"] = limit
}
if transferType != "" {
req["type"] = transferType
}
var resp []TransferHistory
return resp, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, coinbaseproTransfers, req, &resp)
}
// SendHTTPRequest sends an unauthenticated HTTP request
func (e *Exchange) SendHTTPRequest(ctx context.Context, ep exchange.URL, path string, result any) error {
endpoint, err := e.API.Endpoints.GetURL(ep)
if err != nil {
return err
}
item := &request.Item{
Method: http.MethodGet,
Path: endpoint + path,
Result: result,
Verbose: e.Verbose,
HTTPDebugging: e.HTTPDebugging,
HTTPRecording: e.HTTPRecording,
HTTPMockDataSliceLimit: e.HTTPMockDataSliceLimit,
}
return e.SendPayload(ctx, request.UnAuth, func() (*request.Item, error) {
return item, nil
}, request.UnauthenticatedRequest)
}
// SendAuthenticatedHTTPRequest sends an authenticated HTTP request
func (e *Exchange) SendAuthenticatedHTTPRequest(ctx context.Context, ep exchange.URL, method, path string, params map[string]any, result any) (err error) {
creds, err := e.GetCredentials(ctx)
if err != nil {
return err
}
endpoint, err := e.API.Endpoints.GetURL(ep)
if err != nil {
return err
}
newRequest := func() (*request.Item, error) {
payload := []byte("")
if params != nil {
payload, err = json.Marshal(params)
if err != nil {
return nil, err
}
}
n := strconv.FormatInt(time.Now().Unix(), 10)
message := n + method + "/" + path + string(payload)
hmac, err := crypto.GetHMAC(crypto.HashSHA256, []byte(message), []byte(creds.Secret))
if err != nil {
return nil, err
}
headers := make(map[string]string)
headers["CB-ACCESS-SIGN"] = base64.StdEncoding.EncodeToString(hmac)
headers["CB-ACCESS-TIMESTAMP"] = n
headers["CB-ACCESS-KEY"] = creds.Key
headers["CB-ACCESS-PASSPHRASE"] = creds.ClientID
headers["Content-Type"] = "application/json"
return &request.Item{
Method: method,
Path: endpoint + path,
Headers: headers,
Body: bytes.NewBuffer(payload),
Result: result,
Verbose: e.Verbose,
HTTPDebugging: e.HTTPDebugging,
HTTPRecording: e.HTTPRecording,
HTTPMockDataSliceLimit: e.HTTPMockDataSliceLimit,
}, nil
}
return e.SendPayload(ctx, request.Unset, newRequest, request.AuthenticatedRequest)
}
// GetFee returns an estimate of fee based on type of transaction
func (e *Exchange) GetFee(ctx context.Context, feeBuilder *exchange.FeeBuilder) (float64, error) {
var fee float64
switch feeBuilder.FeeType {
case exchange.CryptocurrencyTradeFee:
trailingVolume, err := e.GetTrailingVolume(ctx)
if err != nil {
return 0, err
}
fee = e.calculateTradingFee(trailingVolume,
feeBuilder.Pair.Base,
feeBuilder.Pair.Quote,
feeBuilder.Pair.Delimiter,
feeBuilder.PurchasePrice,
feeBuilder.Amount,
feeBuilder.IsMaker)
case exchange.InternationalBankWithdrawalFee:
fee = getInternationalBankWithdrawalFee(feeBuilder.FiatCurrency)
case exchange.InternationalBankDepositFee:
fee = getInternationalBankDepositFee(feeBuilder.FiatCurrency)
case exchange.OfflineTradeFee:
fee = getOfflineTradeFee(feeBuilder.PurchasePrice, feeBuilder.Amount)
}
if fee < 0 {
fee = 0
}
return fee, nil
}
// getOfflineTradeFee calculates the worst case-scenario trading fee
func getOfflineTradeFee(price, amount float64) float64 {
return 0.0025 * price * amount
}
func (e *Exchange) calculateTradingFee(trailingVolume []Volume, base, quote currency.Code, delimiter string, purchasePrice, amount float64, isMaker bool) float64 {
var fee float64
for _, i := range trailingVolume {
if strings.EqualFold(i.ProductID, base.String()+delimiter+quote.String()) {
switch {
case isMaker:
fee = 0
case i.Volume <= 10000000:
fee = 0.003
case i.Volume > 10000000 && i.Volume <= 100000000:
fee = 0.002
case i.Volume > 100000000:
fee = 0.001
}
break
}
}
return fee * amount * purchasePrice
}
func getInternationalBankWithdrawalFee(c currency.Code) float64 {
var fee float64
if c.Equal(currency.USD) {
fee = 25
} else if c.Equal(currency.EUR) {
fee = 0.15
}
return fee
}
func getInternationalBankDepositFee(c currency.Code) float64 {
var fee float64
if c.Equal(currency.USD) {
fee = 10
} else if c.Equal(currency.EUR) {
fee = 0.15
}
return fee
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,514 +0,0 @@
package coinbasepro
import (
"time"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/encoding/json"
"github.com/thrasher-corp/gocryptotrader/types"
)
// Product holds product information
type Product struct {
ID string `json:"id"`
BaseCurrency string `json:"base_currency"`
QuoteCurrency string `json:"quote_currency"`
QuoteIncrement float64 `json:"quote_increment,string"`
BaseIncrement float64 `json:"base_increment,string"`
DisplayName string `json:"display_name"`
MinimumMarketFunds float64 `json:"min_market_funds,string"`
MarginEnabled bool `json:"margin_enabled"`
PostOnly bool `json:"post_only"`
LimitOnly bool `json:"limit_only"`
CancelOnly bool `json:"cancel_only"`
Status string `json:"status"`
StatusMessage string `json:"status_message"`
TradingDisabled bool `json:"trading_disabled"`
ForeignExchangeStableCoin bool `json:"fx_stablecoin"`
MaxSlippagePercentage float64 `json:"max_slippage_percentage,string"`
AuctionMode bool `json:"auction_mode"`
}
// Ticker holds basic ticker information
type Ticker struct {
TradeID int64 `json:"trade_id"`
Ask float64 `json:"ask,string"`
Bid float64 `json:"bid,string"`
Price float64 `json:"price,string"`
Size float64 `json:"size,string"`
Volume float64 `json:"volume,string"`
Time time.Time `json:"time"`
}
// Trade holds executed trade information
type Trade struct {
TradeID int64 `json:"trade_id"`
Price float64 `json:"price,string"`
Size float64 `json:"size,string"`
Time time.Time `json:"time"`
Side string `json:"side"`
}
// History holds historic rate information
type History struct {
Time types.Time
Low float64
High float64
Open float64
Close float64
Volume float64
}
// UnmarshalJSON deserilizes kline data from a JSON array into History fields.
func (h *History) UnmarshalJSON(data []byte) error {
return json.Unmarshal(data, &[6]any{&h.Time, &h.Low, &h.High, &h.Open, &h.Close, &h.Volume})
}
// Stats holds last 24 hr data for coinbasepro
type Stats struct {
Open float64 `json:"open,string"`
High float64 `json:"high,string"`
Low float64 `json:"low,string"`
Volume float64 `json:"volume,string"`
Last float64 `json:"last,string"`
Volume30Day float64 `json:"volume_30day,string"`
}
// Currency holds singular currency product information
type Currency struct {
ID string
Name string
MinSize float64 `json:"min_size,string"`
}
// ServerTime holds current requested server time information
type ServerTime struct {
ISO time.Time `json:"iso"`
Epoch float64 `json:"epoch"`
}
// AccountResponse holds the details for the trading accounts
type AccountResponse struct {
ID string `json:"id"`
Currency string `json:"currency"`
Balance float64 `json:"balance,string"`
Available float64 `json:"available,string"`
Hold float64 `json:"hold,string"`
ProfileID string `json:"profile_id"`
MarginEnabled bool `json:"margin_enabled"`
FundedAmount float64 `json:"funded_amount,string"`
DefaultAmount float64 `json:"default_amount,string"`
}
// AccountLedgerResponse holds account history information
type AccountLedgerResponse struct {
ID string `json:"id"`
CreatedAt time.Time `json:"created_at"`
Amount float64 `json:"amount,string"`
Balance float64 `json:"balance,string"`
Type string `json:"type"`
Details any `json:"details"`
}
// AccountHolds contains the hold information about an account
type AccountHolds struct {
ID string `json:"id"`
AccountID string `json:"account_id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt string `json:"updated_at"`
Amount float64 `json:"amount,string"`
Type string `json:"type"`
Reference string `json:"ref"`
}
// GeneralizedOrderResponse is the generalized return type across order
// placement and information collation
type GeneralizedOrderResponse struct {
ID string `json:"id"`
Price float64 `json:"price,string"`
Size float64 `json:"size,string"`
ProductID string `json:"product_id"`
Side string `json:"side"`
Stp string `json:"stp"`
Type string `json:"type"`
TimeInForce string `json:"time_in_force"`
PostOnly bool `json:"post_only"`
CreatedAt time.Time `json:"created_at"`
FillFees float64 `json:"fill_fees,string"`
FilledSize float64 `json:"filled_size,string"`
ExecutedValue float64 `json:"executed_value,string"`
Status string `json:"status"`
Settled bool `json:"settled"`
Funds float64 `json:"funds,string"`
SpecifiedFunds float64 `json:"specified_funds,string"`
DoneReason string `json:"done_reason"`
DoneAt time.Time `json:"done_at"`
}
// Funding holds funding data
type Funding struct {
ID string `json:"id"`
OrderID string `json:"order_id"`
ProfileID string `json:"profile_id"`
Amount float64 `json:"amount,string"`
Status string `json:"status"`
CreatedAt time.Time `json:"created_at"`
Currency string `json:"currency"`
RepaidAmount float64 `json:"repaid_amount"`
DefaultAmount float64 `json:"default_amount,string"`
RepaidDefault bool `json:"repaid_default"`
}
// MarginTransfer holds margin transfer details
type MarginTransfer struct {
CreatedAt time.Time `json:"created_at"`
ID string `json:"id"`
UserID string `json:"user_id"`
ProfileID string `json:"profile_id"`
MarginProfileID string `json:"margin_profile_id"`
Type string `json:"type"`
Amount float64 `json:"amount,string"`
Currency string `json:"currency"`
AccountID string `json:"account_id"`
MarginAccountID string `json:"margin_account_id"`
MarginProductID string `json:"margin_product_id"`
Status string `json:"status"`
Nonce int `json:"nonce"`
}
// AccountOverview holds account information returned from position
type AccountOverview struct {
Status string `json:"status"`
Funding struct {
MaxFundingValue float64 `json:"max_funding_value,string"`
FundingValue float64 `json:"funding_value,string"`
OldestOutstanding struct {
ID string `json:"id"`
OrderID string `json:"order_id"`
CreatedAt time.Time `json:"created_at"`
Currency string `json:"currency"`
AccountID string `json:"account_id"`
Amount float64 `json:"amount,string"`
} `json:"oldest_outstanding"`
} `json:"funding"`
Accounts struct {
LTC Account `json:"LTC"`
ETH Account `json:"ETH"`
USD Account `json:"USD"`
BTC Account `json:"BTC"`
} `json:"accounts"`
MarginCall struct {
Active bool `json:"active"`
Price float64 `json:"price,string"`
Side string `json:"side"`
Size float64 `json:"size,string"`
Funds float64 `json:"funds,string"`
} `json:"margin_call"`
UserID string `json:"user_id"`
ProfileID string `json:"profile_id"`
Position struct {
Type string `json:"type"`
Size float64 `json:"size,string"`
Complement float64 `json:"complement,string"`
MaxSize float64 `json:"max_size,string"`
} `json:"position"`
ProductID string `json:"product_id"`
}
// Account is a sub-type for account overview
type Account struct {
ID string `json:"id"`
Balance float64 `json:"balance,string"`
Hold float64 `json:"hold,string"`
FundedAmount float64 `json:"funded_amount,string"`
DefaultAmount float64 `json:"default_amount,string"`
}
// PaymentMethod holds payment method information
type PaymentMethod struct {
ID string `json:"id"`
Type string `json:"type"`
Name string `json:"name"`
Currency string `json:"currency"`
PrimaryBuy bool `json:"primary_buy"`
PrimarySell bool `json:"primary_sell"`
AllowBuy bool `json:"allow_buy"`
AllowSell bool `json:"allow_sell"`
AllowDeposits bool `json:"allow_deposits"`
AllowWithdraw bool `json:"allow_withdraw"`
Limits struct {
Buy []LimitInfo `json:"buy"`
InstantBuy []LimitInfo `json:"instant_buy"`
Sell []LimitInfo `json:"sell"`
Deposit []LimitInfo `json:"deposit"`
} `json:"limits"`
}
// LimitInfo is a sub-type for payment method
type LimitInfo struct {
PeriodInDays int `json:"period_in_days"`
Total struct {
Amount float64 `json:"amount,string"`
Currency string `json:"currency"`
} `json:"total"`
}
// DepositWithdrawalInfo holds returned deposit information
type DepositWithdrawalInfo struct {
ID string `json:"id"`
Amount float64 `json:"amount,string"`
Currency string `json:"currency"`
PayoutAt time.Time `json:"payout_at"`
}
// CoinbaseAccounts holds coinbase account information
type CoinbaseAccounts struct {
ID string `json:"id"`
Name string `json:"name"`
Balance float64 `json:"balance,string"`
Currency string `json:"currency"`
Type string `json:"type"`
Primary bool `json:"primary"`
Active bool `json:"active"`
WireDepositInformation struct {
AccountNumber string `json:"account_number"`
RoutingNumber string `json:"routing_number"`
BankName string `json:"bank_name"`
BankAddress string `json:"bank_address"`
BankCountry struct {
Code string `json:"code"`
Name string `json:"name"`
} `json:"bank_country"`
AccountName string `json:"account_name"`
AccountAddress string `json:"account_address"`
Reference string `json:"reference"`
} `json:"wire_deposit_information"`
SepaDepositInformation struct {
Iban string `json:"iban"`
Swift string `json:"swift"`
BankName string `json:"bank_name"`
BankAddress string `json:"bank_address"`
BankCountryName string `json:"bank_country_name"`
AccountName string `json:"account_name"`
AccountAddress string `json:"account_address"`
Reference string `json:"reference"`
} `json:"sep_deposit_information"`
}
// Report holds historical information
type Report struct {
ID string `json:"id"`
Type string `json:"type"`
Status string `json:"status"`
CreatedAt time.Time `json:"created_at"`
CompletedAt time.Time `json:"completed_at"`
ExpiresAt time.Time `json:"expires_at"`
FileURL string `json:"file_url"`
Params struct {
StartDate time.Time `json:"start_date"`
EndDate time.Time `json:"end_date"`
} `json:"params"`
}
// Volume type contains trailing volume information
type Volume struct {
ProductID string `json:"product_id"`
ExchangeVolume float64 `json:"exchange_volume,string"`
Volume float64 `json:"volume,string"`
RecordedAt string `json:"recorded_at"`
}
// OrderL1L2 is a type used in layer conversion
type OrderL1L2 struct {
Price float64
Amount float64
NumOrders float64
}
// OrderL3 is a type used in layer conversion
type OrderL3 struct {
Price float64
Amount float64
OrderID string
}
// OrderbookL1L2 holds level 1 and 2 order book information
type OrderbookL1L2 struct {
Sequence int64 `json:"sequence"`
Bids []OrderL1L2 `json:"bids"`
Asks []OrderL1L2 `json:"asks"`
}
// OrderbookL3 holds level 3 order book information
type OrderbookL3 struct {
Sequence int64 `json:"sequence"`
Bids []OrderL3 `json:"bids"`
Asks []OrderL3 `json:"asks"`
}
// OrderbookResponse is a generalized response for order books
type OrderbookResponse struct {
Sequence int64 `json:"sequence"`
Bids [][3]types.Number `json:"bids"`
Asks [][3]types.Number `json:"asks"`
}
// FillResponse contains fill information from the exchange
type FillResponse struct {
TradeID int64 `json:"trade_id"`
ProductID string `json:"product_id"`
Price float64 `json:"price,string"`
Size float64 `json:"size,string"`
OrderID string `json:"order_id"`
CreatedAt time.Time `json:"created_at"`
Liquidity string `json:"liquidity"`
Fee float64 `json:"fee,string"`
Settled bool `json:"settled"`
Side string `json:"side"`
}
// WebsocketSubscribe takes in subscription information
type WebsocketSubscribe struct {
Type string `json:"type"`
ProductIDs []string `json:"product_ids,omitempty"`
Channels []any `json:"channels,omitempty"`
Signature string `json:"signature,omitempty"`
Key string `json:"key,omitempty"`
Passphrase string `json:"passphrase,omitempty"`
Timestamp string `json:"timestamp,omitempty"`
}
// WsChannel defines a websocket subscription channel
type WsChannel struct {
Name string `json:"name"`
ProductIDs []string `json:"product_ids,omitempty"`
}
// wsOrderReceived holds websocket received values
type wsOrderReceived struct {
Type string `json:"type"`
OrderID string `json:"order_id"`
OrderType string `json:"order_type"`
Size float64 `json:"size,string"`
Price float64 `json:"price,omitempty,string"`
Funds float64 `json:"funds,omitempty,string"`
Side string `json:"side"`
ClientOID string `json:"client_oid"`
ProductID string `json:"product_id"`
Sequence int64 `json:"sequence"`
Time time.Time `json:"time"`
RemainingSize float64 `json:"remaining_size,string"`
NewSize float64 `json:"new_size,string"`
OldSize float64 `json:"old_size,string"`
Reason string `json:"reason"`
Timestamp types.Time `json:"timestamp"`
UserID string `json:"user_id"`
ProfileID string `json:"profile_id"`
StopType string `json:"stop_type"`
StopPrice float64 `json:"stop_price,string"`
TakerFeeRate float64 `json:"taker_fee_rate,string"`
Private bool `json:"private"`
TradeID int64 `json:"trade_id"`
MakerOrderID string `json:"maker_order_id"`
TakerOrderID string `json:"taker_order_id"`
TakerUserID string `json:"taker_user_id"`
}
// WebsocketHeartBeat defines JSON response for a heart beat message
type WebsocketHeartBeat struct {
Type string `json:"type"`
Sequence int64 `json:"sequence"`
LastTradeID int64 `json:"last_trade_id"`
ProductID string `json:"product_id"`
Time time.Time `json:"time"`
}
// WebsocketTicker defines ticker websocket response
type WebsocketTicker struct {
Type string `json:"type"`
Sequence int64 `json:"sequence"`
ProductID currency.Pair `json:"product_id"`
Price float64 `json:"price,string"`
Open24H float64 `json:"open_24h,string"`
Volume24H float64 `json:"volume_24h,string"`
Low24H float64 `json:"low_24h,string"`
High24H float64 `json:"high_24h,string"`
Volume30D float64 `json:"volume_30d,string"`
BestBid float64 `json:"best_bid,string"`
BestAsk float64 `json:"best_ask,string"`
Side string `json:"side"`
Time time.Time `json:"time"`
TradeID int64 `json:"trade_id"`
LastSize float64 `json:"last_size,string"`
}
// WebsocketOrderbookSnapshot defines a snapshot response
type WebsocketOrderbookSnapshot struct {
ProductID string `json:"product_id"`
Type string `json:"type"`
Bids [][2]types.Number `json:"bids"`
Asks [][2]types.Number `json:"asks"`
Time time.Time `json:"time"`
}
// WebsocketL2Update defines an update on the L2 orderbooks
type WebsocketL2Update struct {
Type string `json:"type"`
ProductID string `json:"product_id"`
Time time.Time `json:"time"`
Changes [][3]string `json:"changes"`
}
type wsMsgType struct {
Type string `json:"type"`
Sequence int64 `json:"sequence"`
ProductID string `json:"product_id"`
}
type wsStatus struct {
Currencies []struct {
ConvertibleTo []string `json:"convertible_to"`
Details struct{} `json:"details"`
ID string `json:"id"`
MaxPrecision float64 `json:"max_precision,string"`
MinSize float64 `json:"min_size,string"`
Name string `json:"name"`
Status string `json:"status"`
StatusMessage any `json:"status_message"`
} `json:"currencies"`
Products []struct {
BaseCurrency string `json:"base_currency"`
BaseIncrement float64 `json:"base_increment,string"`
BaseMaxSize float64 `json:"base_max_size,string"`
BaseMinSize float64 `json:"base_min_size,string"`
CancelOnly bool `json:"cancel_only"`
DisplayName string `json:"display_name"`
ID string `json:"id"`
LimitOnly bool `json:"limit_only"`
MaxMarketFunds float64 `json:"max_market_funds,string"`
MinMarketFunds float64 `json:"min_market_funds,string"`
PostOnly bool `json:"post_only"`
QuoteCurrency string `json:"quote_currency"`
QuoteIncrement float64 `json:"quote_increment,string"`
Status string `json:"status"`
StatusMessage any `json:"status_message"`
} `json:"products"`
Type string `json:"type"`
}
// TransferHistory returns wallet transfer history
type TransferHistory struct {
ID string `json:"id"`
Type string `json:"type"`
CreatedAt string `json:"created_at"`
CompletedAt string `json:"completed_at"`
CanceledAt time.Time `json:"canceled_at"`
ProcessedAt time.Time `json:"processed_at"`
UserNonce int64 `json:"user_nonce"`
Amount string `json:"amount"`
Details struct {
CoinbaseAccountID string `json:"coinbase_account_id"`
CoinbaseTransactionID string `json:"coinbase_transaction_id"`
CoinbasePaymentMethodID string `json:"coinbase_payment_method_id"`
} `json:"details"`
}

View File

@@ -1,455 +0,0 @@
package coinbasepro
import (
"context"
"encoding/base64"
"errors"
"fmt"
"net/http"
"strconv"
"time"
gws "github.com/gorilla/websocket"
"github.com/thrasher-corp/gocryptotrader/common/crypto"
"github.com/thrasher-corp/gocryptotrader/currency"
"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/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"github.com/thrasher-corp/gocryptotrader/exchanges/subscription"
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
"github.com/thrasher-corp/gocryptotrader/exchanges/trade"
)
const (
coinbaseproWebsocketURL = "wss://ws-feed.pro.coinbase.com"
)
// 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
err := e.Websocket.Conn.Dial(ctx, &dialer, http.Header{})
if err != nil {
return err
}
e.Websocket.Wg.Add(1)
go e.wsReadData(ctx)
return nil
}
// wsReadData receives and passes on websocket messages for processing
func (e *Exchange) wsReadData(ctx context.Context) {
defer e.Websocket.Wg.Done()
for {
resp := e.Websocket.Conn.ReadMessage()
if resp.Raw == nil {
return
}
err := e.wsHandleData(ctx, resp.Raw)
if err != nil {
e.Websocket.DataHandler <- err
}
}
}
func (e *Exchange) wsHandleData(ctx context.Context, respRaw []byte) error {
msgType := wsMsgType{}
err := json.Unmarshal(respRaw, &msgType)
if err != nil {
return err
}
if msgType.Type == "subscriptions" || msgType.Type == "heartbeat" {
return nil
}
switch msgType.Type {
case "status":
var status wsStatus
err = json.Unmarshal(respRaw, &status)
if err != nil {
return err
}
e.Websocket.DataHandler <- status
case "error":
e.Websocket.DataHandler <- errors.New(string(respRaw))
case "ticker":
wsTicker := WebsocketTicker{}
err := json.Unmarshal(respRaw, &wsTicker)
if err != nil {
return err
}
e.Websocket.DataHandler <- &ticker.Price{
LastUpdated: wsTicker.Time,
Pair: wsTicker.ProductID,
AssetType: asset.Spot,
ExchangeName: e.Name,
Open: wsTicker.Open24H,
High: wsTicker.High24H,
Low: wsTicker.Low24H,
Last: wsTicker.Price,
Volume: wsTicker.Volume24H,
Bid: wsTicker.BestBid,
Ask: wsTicker.BestAsk,
}
case "snapshot":
var snapshot WebsocketOrderbookSnapshot
err := json.Unmarshal(respRaw, &snapshot)
if err != nil {
return err
}
err = e.ProcessSnapshot(&snapshot)
if err != nil {
return err
}
case "l2update":
var update WebsocketL2Update
err := json.Unmarshal(respRaw, &update)
if err != nil {
return err
}
err = e.ProcessOrderbookUpdate(&update)
if err != nil {
return err
}
case "received", "open", "done", "change", "activate":
var wsOrder wsOrderReceived
err := json.Unmarshal(respRaw, &wsOrder)
if err != nil {
return err
}
var oType order.Type
var oSide order.Side
var oStatus order.Status
oType, err = order.StringToOrderType(wsOrder.OrderType)
if err != nil {
e.Websocket.DataHandler <- order.ClassificationError{
Exchange: e.Name,
OrderID: wsOrder.OrderID,
Err: err,
}
}
oSide, err = order.StringToOrderSide(wsOrder.Side)
if err != nil {
e.Websocket.DataHandler <- order.ClassificationError{
Exchange: e.Name,
OrderID: wsOrder.OrderID,
Err: err,
}
}
oStatus, err = statusToStandardStatus(wsOrder.Type)
if err != nil {
e.Websocket.DataHandler <- order.ClassificationError{
Exchange: e.Name,
OrderID: wsOrder.OrderID,
Err: err,
}
}
if wsOrder.Reason == "canceled" {
oStatus = order.Cancelled
}
ts := wsOrder.Time
if wsOrder.Type == "activate" {
ts = wsOrder.Timestamp.Time()
}
creds, err := e.GetCredentials(ctx)
if err != nil {
e.Websocket.DataHandler <- order.ClassificationError{
Exchange: e.Name,
OrderID: wsOrder.OrderID,
Err: err,
}
}
clientID := ""
if creds != nil {
clientID = creds.ClientID
}
if wsOrder.UserID != "" {
var p currency.Pair
var a asset.Item
p, a, err = e.GetRequestFormattedPairAndAssetType(wsOrder.ProductID)
if err != nil {
return err
}
e.Websocket.DataHandler <- &order.Detail{
HiddenOrder: wsOrder.Private,
Price: wsOrder.Price,
Amount: wsOrder.Size,
TriggerPrice: wsOrder.StopPrice,
ExecutedAmount: wsOrder.Size - wsOrder.RemainingSize,
RemainingAmount: wsOrder.RemainingSize,
Fee: wsOrder.TakerFeeRate,
Exchange: e.Name,
OrderID: wsOrder.OrderID,
AccountID: wsOrder.ProfileID,
ClientID: clientID,
Type: oType,
Side: oSide,
Status: oStatus,
AssetType: a,
Date: ts,
Pair: p,
}
}
case "match", "last_match":
var wsOrder wsOrderReceived
err := json.Unmarshal(respRaw, &wsOrder)
if err != nil {
return err
}
oSide, err := order.StringToOrderSide(wsOrder.Side)
if err != nil {
e.Websocket.DataHandler <- order.ClassificationError{
Exchange: e.Name,
Err: err,
}
}
var p currency.Pair
var a asset.Item
p, a, err = e.GetRequestFormattedPairAndAssetType(wsOrder.ProductID)
if err != nil {
return err
}
if wsOrder.UserID != "" {
e.Websocket.DataHandler <- &order.Detail{
OrderID: wsOrder.OrderID,
Pair: p,
AssetType: a,
Trades: []order.TradeHistory{
{
Price: wsOrder.Price,
Amount: wsOrder.Size,
Exchange: e.Name,
TID: strconv.FormatInt(wsOrder.TradeID, 10),
Side: oSide,
Timestamp: wsOrder.Time,
IsMaker: wsOrder.TakerUserID == "",
},
},
}
} else {
if !e.IsSaveTradeDataEnabled() {
return nil
}
return trade.AddTradesToBuffer(trade.Data{
Timestamp: wsOrder.Time,
Exchange: e.Name,
CurrencyPair: p,
AssetType: a,
Price: wsOrder.Price,
Amount: wsOrder.Size,
Side: oSide,
TID: strconv.FormatInt(wsOrder.TradeID, 10),
})
}
default:
e.Websocket.DataHandler <- websocket.UnhandledMessageWarning{Message: e.Name + websocket.UnhandledMessage + string(respRaw)}
return nil
}
return nil
}
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("%s not recognised as status type", stat)
}
}
// ProcessSnapshot processes the initial orderbook snap shot
func (e *Exchange) ProcessSnapshot(snapshot *WebsocketOrderbookSnapshot) error {
pair, err := currency.NewPairFromString(snapshot.ProductID)
if err != nil {
return err
}
ob := &orderbook.Book{
Pair: pair,
Bids: make(orderbook.Levels, len(snapshot.Bids)),
Asks: make(orderbook.Levels, len(snapshot.Asks)),
Asset: asset.Spot,
Exchange: e.Name,
ValidateOrderbook: e.ValidateOrderbook,
LastUpdated: snapshot.Time,
}
for i := range snapshot.Bids {
ob.Bids[i].Price = snapshot.Bids[i][0].Float64()
ob.Bids[i].Amount = snapshot.Bids[i][1].Float64()
}
for i := range snapshot.Asks {
ob.Asks[i].Price = snapshot.Asks[i][0].Float64()
ob.Asks[i].Amount = snapshot.Asks[i][1].Float64()
}
return e.Websocket.Orderbook.LoadSnapshot(ob)
}
// ProcessOrderbookUpdate updates the orderbook local cache
func (e *Exchange) ProcessOrderbookUpdate(update *WebsocketL2Update) error {
if len(update.Changes) == 0 {
return errors.New("no data in websocket update")
}
p, err := currency.NewPairFromString(update.ProductID)
if err != nil {
return err
}
asks := make(orderbook.Levels, 0, len(update.Changes))
bids := make(orderbook.Levels, 0, len(update.Changes))
for i := range update.Changes {
price, err := strconv.ParseFloat(update.Changes[i][1], 64)
if err != nil {
return err
}
volume, err := strconv.ParseFloat(update.Changes[i][2], 64)
if err != nil {
return err
}
if update.Changes[i][0] == order.Buy.Lower() {
bids = append(bids, orderbook.Level{Price: price, Amount: volume})
} else {
asks = append(asks, orderbook.Level{Price: price, Amount: volume})
}
}
return e.Websocket.Orderbook.Update(&orderbook.Update{
Bids: bids,
Asks: asks,
Pair: p,
UpdateTime: update.Time,
Asset: asset.Spot,
})
}
// generateSubscriptions returns a list of subscriptions from the configured subscriptions feature
func (e *Exchange) generateSubscriptions() (subscription.List, error) {
pairs, err := e.GetEnabledPairs(asset.Spot)
if err != nil {
return nil, err
}
pairFmt, err := e.GetPairFormat(asset.Spot, true)
if err != nil {
return nil, err
}
pairs = pairs.Format(pairFmt)
authed := e.IsWebsocketAuthenticationSupported()
subs := make(subscription.List, 0, len(e.Features.Subscriptions))
for _, baseSub := range e.Features.Subscriptions {
if !authed && baseSub.Authenticated {
continue
}
s := baseSub.Clone()
s.Asset = asset.Spot
s.Pairs = pairs
subs = append(subs, s)
}
return subs, nil
}
// Subscribe sends a websocket message to receive data from the channel
func (e *Exchange) Subscribe(subs subscription.List) error {
ctx := context.TODO()
r := &WebsocketSubscribe{
Type: "subscribe",
Channels: make([]any, 0, len(subs)),
}
// See if we have a consistent Pair list for all the subs that we can use globally
// If all the subs have the same pairs then we can use the top level ProductIDs field
// Otherwise each and every sub needs to have it's own list
for i, s := range subs {
if i == 0 {
r.ProductIDs = s.Pairs.Strings()
} else if !subs[0].Pairs.Equal(s.Pairs) {
r.ProductIDs = nil
break
}
}
for _, s := range subs {
if s.Authenticated && r.Key == "" && e.IsWebsocketAuthenticationSupported() {
if err := e.authWsSubscibeReq(ctx, r); err != nil {
return err
}
}
if len(r.ProductIDs) == 0 {
r.Channels = append(r.Channels, WsChannel{
Name: s.Channel,
ProductIDs: s.Pairs.Strings(),
})
} else {
// Coinbase does not support using [WsChannel{Name:"x"}] unless each ProductIDs field is populated
// Therefore we have to use Channels as an array of strings
r.Channels = append(r.Channels, s.Channel)
}
}
err := e.Websocket.Conn.SendJSONMessage(ctx, request.Unset, r)
if err == nil {
err = e.Websocket.AddSuccessfulSubscriptions(e.Websocket.Conn, subs...)
}
return err
}
func (e *Exchange) authWsSubscibeReq(ctx context.Context, r *WebsocketSubscribe) error {
creds, err := e.GetCredentials(ctx)
if err != nil {
return err
}
r.Timestamp = strconv.FormatInt(time.Now().Unix(), 10)
message := r.Timestamp + http.MethodGet + "/users/self/verify"
hmac, err := crypto.GetHMAC(crypto.HashSHA256, []byte(message), []byte(creds.Secret))
if err != nil {
return err
}
r.Signature = base64.StdEncoding.EncodeToString(hmac)
r.Key = creds.Key
r.Passphrase = creds.ClientID
return nil
}
// Unsubscribe sends a websocket message to stop receiving data from the channel
func (e *Exchange) Unsubscribe(subs subscription.List) error {
ctx := context.TODO()
r := &WebsocketSubscribe{
Type: "unsubscribe",
Channels: make([]any, 0, len(subs)),
}
for _, s := range subs {
r.Channels = append(r.Channels, WsChannel{
Name: s.Channel,
ProductIDs: s.Pairs.Strings(),
})
}
err := e.Websocket.Conn.SendJSONMessage(ctx, request.Unset, r)
if err == nil {
err = e.Websocket.RemoveSubscriptions(e.Websocket.Conn, subs...)
}
return err
}

View File

@@ -1,873 +0,0 @@
package coinbasepro
import (
"context"
"fmt"
"sort"
"strconv"
"time"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/config"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchange/websocket"
"github.com/thrasher-corp/gocryptotrader/exchange/websocket/buffer"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/account"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/deposit"
"github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate"
"github.com/thrasher-corp/gocryptotrader/exchanges/futures"
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
"github.com/thrasher-corp/gocryptotrader/exchanges/protocol"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"github.com/thrasher-corp/gocryptotrader/exchanges/subscription"
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
"github.com/thrasher-corp/gocryptotrader/exchanges/trade"
"github.com/thrasher-corp/gocryptotrader/log"
"github.com/thrasher-corp/gocryptotrader/portfolio/withdraw"
)
// SetDefaults sets default values for the exchange
func (e *Exchange) SetDefaults() {
e.Name = "CoinbasePro"
e.Enabled = true
e.Verbose = true
e.API.CredentialsValidator.RequiresKey = true
e.API.CredentialsValidator.RequiresSecret = true
e.API.CredentialsValidator.RequiresClientID = true
e.API.CredentialsValidator.RequiresBase64DecodeSecret = true
requestFmt := &currency.PairFormat{Delimiter: currency.DashDelimiter, Uppercase: true}
configFmt := &currency.PairFormat{Delimiter: currency.DashDelimiter, Uppercase: true}
err := e.SetGlobalPairsManager(requestFmt, configFmt, asset.Spot)
if err != nil {
log.Errorln(log.ExchangeSys, err)
}
e.Features = exchange.Features{
Supports: exchange.FeaturesSupported{
REST: true,
Websocket: true,
RESTCapabilities: protocol.Features{
TickerFetching: true,
KlineFetching: true,
TradeFetching: true,
OrderbookFetching: true,
AutoPairUpdates: true,
AccountInfo: true,
GetOrder: true,
GetOrders: true,
CancelOrders: true,
CancelOrder: true,
SubmitOrder: true,
DepositHistory: true,
WithdrawalHistory: true,
UserTradeHistory: true,
CryptoDeposit: true,
CryptoWithdrawal: true,
FiatDeposit: true,
FiatWithdraw: true,
TradeFee: true,
FiatDepositFee: true,
FiatWithdrawalFee: true,
CandleHistory: true,
},
WebsocketCapabilities: protocol.Features{
TickerFetching: true,
OrderbookFetching: true,
Subscribe: true,
Unsubscribe: true,
AuthenticatedEndpoints: true,
MessageSequenceNumbers: true,
GetOrders: true,
GetOrder: true,
},
WithdrawPermissions: exchange.AutoWithdrawCryptoWithAPIPermission |
exchange.AutoWithdrawFiatWithAPIPermission,
Kline: kline.ExchangeCapabilitiesSupported{
DateRanges: true,
Intervals: true,
},
},
Enabled: exchange.FeaturesEnabled{
AutoPairUpdates: true,
Kline: kline.ExchangeCapabilitiesEnabled{
Intervals: kline.DeployExchangeIntervals(
kline.IntervalCapacity{Interval: kline.OneMin},
kline.IntervalCapacity{Interval: kline.FiveMin},
kline.IntervalCapacity{Interval: kline.FifteenMin},
kline.IntervalCapacity{Interval: kline.OneHour},
kline.IntervalCapacity{Interval: kline.SixHour},
kline.IntervalCapacity{Interval: kline.OneDay},
),
GlobalResultLimit: 300,
},
},
Subscriptions: subscription.List{
{Enabled: true, Channel: "heartbeat"},
{Enabled: true, Channel: "level2_batch"}, // Other orderbook feeds require authentication; This is batched in 50ms lots
{Enabled: true, Channel: "ticker"},
{Enabled: true, Channel: "user", Authenticated: true},
{Enabled: true, Channel: "matches"},
},
}
e.Requester, err = request.New(e.Name,
common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout),
request.WithLimiter(GetRateLimit()))
if err != nil {
log.Errorln(log.ExchangeSys, err)
}
e.API.Endpoints = e.NewEndpoints()
err = e.API.Endpoints.SetDefaultEndpoints(map[exchange.URL]string{
exchange.RestSpot: coinbaseproAPIURL,
exchange.RestSandbox: coinbaseproSandboxAPIURL,
exchange.WebsocketSpot: coinbaseproWebsocketURL,
})
if err != nil {
log.Errorln(log.ExchangeSys, err)
}
e.Websocket = websocket.NewManager()
e.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit
e.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout
e.WebsocketOrderbookBufferLimit = exchange.DefaultWebsocketOrderbookBufferLimit
}
// Setup initialises the exchange parameters with the current configuration
func (e *Exchange) Setup(exch *config.Exchange) error {
err := exch.Validate()
if err != nil {
return err
}
if !exch.Enabled {
e.SetEnabled(false)
return nil
}
err = e.SetupDefaults(exch)
if err != nil {
return err
}
wsRunningURL, err := e.API.Endpoints.GetURL(exchange.WebsocketSpot)
if err != nil {
return err
}
err = e.Websocket.Setup(&websocket.ManagerSetup{
ExchangeConfig: exch,
DefaultURL: coinbaseproWebsocketURL,
RunningURL: wsRunningURL,
Connector: e.WsConnect,
Subscriber: e.Subscribe,
Unsubscriber: e.Unsubscribe,
GenerateSubscriptions: e.generateSubscriptions,
Features: &e.Features.Supports.WebsocketCapabilities,
OrderbookBufferConfig: buffer.Config{
SortBuffer: true,
},
})
if err != nil {
return err
}
return e.Websocket.SetupNewConnection(&websocket.ConnectionSetup{
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
ResponseMaxLimit: exch.WebsocketResponseMaxLimit,
})
}
// FetchTradablePairs returns a list of the exchanges tradable pairs
func (e *Exchange) FetchTradablePairs(ctx context.Context, _ asset.Item) (currency.Pairs, error) {
products, err := e.GetProducts(ctx)
if err != nil {
return nil, err
}
pairs := make([]currency.Pair, 0, len(products))
for x := range products {
if products[x].TradingDisabled {
continue
}
var pair currency.Pair
pair, err = currency.NewPairDelimiter(products[x].ID, currency.DashDelimiter)
if err != nil {
return nil, err
}
pairs = append(pairs, pair)
}
return pairs, nil
}
// UpdateTradablePairs updates the exchanges available pairs and stores
// them in the exchanges config
func (e *Exchange) UpdateTradablePairs(ctx context.Context, forceUpdate bool) error {
pairs, err := e.FetchTradablePairs(ctx, asset.Spot)
if err != nil {
return err
}
err = e.UpdatePairs(pairs, asset.Spot, false, forceUpdate)
if err != nil {
return err
}
return e.EnsureOnePairEnabled()
}
// UpdateAccountInfo retrieves balances for all enabled currencies for the
// coinbasepro exchange
func (e *Exchange) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) {
var response account.Holdings
response.Exchange = e.Name
accountBalance, err := e.GetAccounts(ctx)
if err != nil {
return response, err
}
accountCurrencies := make(map[string][]account.Balance)
for i := range accountBalance {
profileID := accountBalance[i].ProfileID
currencies := accountCurrencies[profileID]
accountCurrencies[profileID] = append(currencies, account.Balance{
Currency: currency.NewCode(accountBalance[i].Currency),
Total: accountBalance[i].Balance,
Hold: accountBalance[i].Hold,
Free: accountBalance[i].Available,
AvailableWithoutBorrow: accountBalance[i].Available - accountBalance[i].FundedAmount,
Borrowed: accountBalance[i].FundedAmount,
})
}
if response.Accounts, err = account.CollectBalances(accountCurrencies, assetType); err != nil {
return account.Holdings{}, err
}
creds, err := e.GetCredentials(ctx)
if err != nil {
return account.Holdings{}, err
}
err = account.Process(&response, creds)
if err != nil {
return account.Holdings{}, err
}
return response, nil
}
// UpdateTickers updates the ticker for all currency pairs of a given asset type
func (e *Exchange) UpdateTickers(_ context.Context, _ asset.Item) error {
return common.ErrFunctionNotSupported
}
// UpdateTicker updates and returns the ticker for a currency pair
func (e *Exchange) UpdateTicker(ctx context.Context, p currency.Pair, a asset.Item) (*ticker.Price, error) {
fPair, err := e.FormatExchangeCurrency(p, a)
if err != nil {
return nil, err
}
tick, err := e.GetTicker(ctx, fPair.String())
if err != nil {
return nil, err
}
stats, err := e.GetStats(ctx, fPair.String())
if err != nil {
return nil, err
}
tickerPrice := &ticker.Price{
Last: stats.Last,
High: stats.High,
Low: stats.Low,
Bid: tick.Bid,
Ask: tick.Ask,
Volume: tick.Volume,
Open: stats.Open,
Pair: p,
LastUpdated: tick.Time,
ExchangeName: e.Name,
AssetType: a,
}
err = ticker.ProcessTicker(tickerPrice)
if err != nil {
return tickerPrice, err
}
return ticker.GetTicker(e.Name, p, a)
}
// UpdateOrderbook updates and returns the orderbook for a currency pair
func (e *Exchange) UpdateOrderbook(ctx context.Context, p currency.Pair, assetType asset.Item) (*orderbook.Book, error) {
if p.IsEmpty() {
return nil, currency.ErrCurrencyPairEmpty
}
if err := e.CurrencyPairs.IsAssetEnabled(assetType); err != nil {
return nil, err
}
book := &orderbook.Book{
Exchange: e.Name,
Pair: p,
Asset: assetType,
ValidateOrderbook: e.ValidateOrderbook,
}
fPair, err := e.FormatExchangeCurrency(p, assetType)
if err != nil {
return book, err
}
orderbookNew, err := e.GetOrderbook(ctx, fPair.String(), 2)
if err != nil {
return book, err
}
obNew, ok := orderbookNew.(OrderbookL1L2)
if !ok {
return book, common.GetTypeAssertError("OrderbookL1L2", orderbookNew)
}
book.Bids = make(orderbook.Levels, len(obNew.Bids))
for x := range obNew.Bids {
book.Bids[x] = orderbook.Level{
Amount: obNew.Bids[x].Amount,
Price: obNew.Bids[x].Price,
}
}
book.Asks = make(orderbook.Levels, len(obNew.Asks))
for x := range obNew.Asks {
book.Asks[x] = orderbook.Level{
Amount: obNew.Asks[x].Amount,
Price: obNew.Asks[x].Price,
}
}
err = book.Process()
if err != nil {
return book, err
}
return orderbook.Get(e.Name, p, assetType)
}
// GetAccountFundingHistory returns funding history, deposits and
// withdrawals
func (e *Exchange) GetAccountFundingHistory(_ context.Context) ([]exchange.FundingHistory, error) {
return nil, common.ErrFunctionNotSupported
}
// GetWithdrawalsHistory returns previous withdrawals data
func (e *Exchange) GetWithdrawalsHistory(_ context.Context, _ currency.Code, _ asset.Item) ([]exchange.WithdrawalHistory, error) {
// while fetching withdrawal history is possible, the API response lacks any useful information
// like the currency withdrawn and thus is unsupported. If that position changes, use GetTransfers(...)
return nil, common.ErrFunctionNotSupported
}
// GetRecentTrades returns the most recent trades for a currency and asset
func (e *Exchange) GetRecentTrades(ctx context.Context, p currency.Pair, assetType asset.Item) ([]trade.Data, error) {
var err error
p, err = e.FormatExchangeCurrency(p, assetType)
if err != nil {
return nil, err
}
var tradeData []Trade
tradeData, err = e.GetTrades(ctx, p.String())
if err != nil {
return nil, err
}
resp := make([]trade.Data, len(tradeData))
for i := range tradeData {
var side order.Side
side, err = order.StringToOrderSide(tradeData[i].Side)
if err != nil {
return nil, err
}
resp[i] = trade.Data{
Exchange: e.Name,
TID: strconv.FormatInt(tradeData[i].TradeID, 10),
CurrencyPair: p,
AssetType: assetType,
Side: side,
Price: tradeData[i].Price,
Amount: tradeData[i].Size,
Timestamp: tradeData[i].Time,
}
}
err = e.AddTradesToBuffer(resp...)
if err != nil {
return nil, err
}
sort.Sort(trade.ByDate(resp))
return resp, nil
}
// GetHistoricTrades returns historic trade data within the timeframe provided
func (e *Exchange) GetHistoricTrades(_ context.Context, _ currency.Pair, _ asset.Item, _, _ time.Time) ([]trade.Data, error) {
return nil, common.ErrFunctionNotSupported
}
// SubmitOrder submits a new order
func (e *Exchange) SubmitOrder(ctx context.Context, s *order.Submit) (*order.SubmitResponse, error) {
if err := s.Validate(e.GetTradingRequirements()); err != nil {
return nil, err
}
fPair, err := e.FormatExchangeCurrency(s.Pair, asset.Spot)
if err != nil {
return nil, err
}
var orderID string
switch s.Type {
case order.Market:
orderID, err = e.PlaceMarketOrder(ctx,
"",
s.Amount,
s.QuoteAmount,
s.Side.Lower(),
fPair.String(),
"")
case order.Limit:
timeInForce := order.GoodTillCancel.String()
if s.TimeInForce == order.ImmediateOrCancel {
timeInForce = order.ImmediateOrCancel.String()
}
orderID, err = e.PlaceLimitOrder(ctx,
"",
s.Price,
s.Amount,
s.Side.Lower(),
timeInForce,
"",
fPair.String(),
"",
false)
default:
err = fmt.Errorf("%w %v", order.ErrUnsupportedOrderType, s.Type)
}
if err != nil {
return nil, err
}
return s.DeriveSubmitResponse(orderID)
}
// ModifyOrder will allow of changing orderbook placement and limit to
// market conversion
func (e *Exchange) ModifyOrder(_ context.Context, _ *order.Modify) (*order.ModifyResponse, error) {
return nil, common.ErrFunctionNotSupported
}
// CancelOrder cancels an order by its corresponding ID number
func (e *Exchange) CancelOrder(ctx context.Context, o *order.Cancel) error {
if err := o.Validate(o.StandardCancel()); err != nil {
return err
}
return e.CancelExistingOrder(ctx, o.OrderID)
}
// CancelBatchOrders cancels an orders by their corresponding ID numbers
func (e *Exchange) CancelBatchOrders(_ context.Context, _ []order.Cancel) (*order.CancelBatchResponse, error) {
return nil, common.ErrFunctionNotSupported
}
// CancelAllOrders cancels all orders associated with a currency pair
func (e *Exchange) CancelAllOrders(ctx context.Context, _ *order.Cancel) (order.CancelAllResponse, error) {
// CancellAllExisting orders returns a list of successful cancellations, we're only interested in failures
_, err := e.CancelAllExistingOrders(ctx, "")
return order.CancelAllResponse{}, err
}
// GetOrderInfo returns order information based on order ID
func (e *Exchange) GetOrderInfo(ctx context.Context, orderID string, _ currency.Pair, _ asset.Item) (*order.Detail, error) {
genOrderDetail, err := e.GetOrder(ctx, orderID)
if err != nil {
return nil, fmt.Errorf("error retrieving order %s : %w", orderID, err)
}
orderStatus, err := order.StringToOrderStatus(genOrderDetail.Status)
if err != nil {
return nil, fmt.Errorf("error parsing order status: %w", err)
}
orderType, err := order.StringToOrderType(genOrderDetail.Type)
if err != nil {
return nil, fmt.Errorf("error parsing order type: %w", err)
}
orderSide, err := order.StringToOrderSide(genOrderDetail.Side)
if err != nil {
return nil, fmt.Errorf("error parsing order side: %w", err)
}
pair, err := currency.NewPairDelimiter(genOrderDetail.ProductID, "-")
if err != nil {
return nil, fmt.Errorf("error parsing order pair: %w", err)
}
response := order.Detail{
Exchange: e.GetName(),
OrderID: genOrderDetail.ID,
Pair: pair,
Side: orderSide,
Type: orderType,
Date: genOrderDetail.DoneAt,
Status: orderStatus,
Price: genOrderDetail.Price,
Amount: genOrderDetail.Size,
ExecutedAmount: genOrderDetail.FilledSize,
RemainingAmount: genOrderDetail.Size - genOrderDetail.FilledSize,
Fee: genOrderDetail.FillFees,
}
fillResponse, err := e.GetFills(ctx, orderID, genOrderDetail.ProductID)
if err != nil {
return nil, fmt.Errorf("error retrieving the order fills: %w", err)
}
for i := range fillResponse {
var fillSide order.Side
fillSide, err = order.StringToOrderSide(fillResponse[i].Side)
if err != nil {
return nil, fmt.Errorf("error parsing order Side: %w", err)
}
response.Trades = append(response.Trades, order.TradeHistory{
Timestamp: fillResponse[i].CreatedAt,
TID: strconv.FormatInt(fillResponse[i].TradeID, 10),
Price: fillResponse[i].Price,
Amount: fillResponse[i].Size,
Exchange: e.GetName(),
Type: orderType,
Side: fillSide,
Fee: fillResponse[i].Fee,
})
}
return &response, nil
}
// GetDepositAddress returns a deposit address for a specified currency
func (e *Exchange) GetDepositAddress(_ context.Context, _ currency.Code, _, _ string) (*deposit.Address, error) {
return nil, common.ErrFunctionNotSupported
}
// WithdrawCryptocurrencyFunds returns a withdrawal ID when a withdrawal is
// submitted
func (e *Exchange) WithdrawCryptocurrencyFunds(ctx context.Context, withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) {
if err := withdrawRequest.Validate(); err != nil {
return nil, err
}
resp, err := e.WithdrawCrypto(ctx,
withdrawRequest.Amount,
withdrawRequest.Currency.String(),
withdrawRequest.Crypto.Address)
if err != nil {
return nil, err
}
return &withdraw.ExchangeResponse{
ID: resp.ID,
}, err
}
// WithdrawFiatFunds returns a withdrawal ID when a withdrawal is
// submitted
func (e *Exchange) WithdrawFiatFunds(ctx context.Context, withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) {
if err := withdrawRequest.Validate(); err != nil {
return nil, err
}
paymentMethods, err := e.GetPayMethods(ctx)
if err != nil {
return nil, err
}
selectedWithdrawalMethod := PaymentMethod{}
for i := range paymentMethods {
if withdrawRequest.Fiat.Bank.BankName == paymentMethods[i].Name {
selectedWithdrawalMethod = paymentMethods[i]
break
}
}
if selectedWithdrawalMethod.ID == "" {
return nil, fmt.Errorf("could not find payment method '%v'. Check the name via the website and try again", withdrawRequest.Fiat.Bank.BankName)
}
resp, err := e.WithdrawViaPaymentMethod(ctx,
withdrawRequest.Amount,
withdrawRequest.Currency.String(),
selectedWithdrawalMethod.ID)
if err != nil {
return nil, err
}
return &withdraw.ExchangeResponse{
Status: resp.ID,
}, nil
}
// WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a
// withdrawal is submitted
func (e *Exchange) WithdrawFiatFundsToInternationalBank(ctx context.Context, withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) {
if err := withdrawRequest.Validate(); err != nil {
return nil, err
}
v, err := e.WithdrawFiatFunds(ctx, withdrawRequest)
if err != nil {
return nil, err
}
return &withdraw.ExchangeResponse{
ID: v.ID,
Status: v.Status,
}, nil
}
// GetFeeByType returns an estimate of fee based on type of transaction
func (e *Exchange) GetFeeByType(ctx context.Context, feeBuilder *exchange.FeeBuilder) (float64, error) {
if feeBuilder == nil {
return 0, fmt.Errorf("%T %w", feeBuilder, common.ErrNilPointer)
}
if !e.AreCredentialsValid(ctx) && // Todo check connection status
feeBuilder.FeeType == exchange.CryptocurrencyTradeFee {
feeBuilder.FeeType = exchange.OfflineTradeFee
}
return e.GetFee(ctx, feeBuilder)
}
// GetActiveOrders retrieves any orders that are active/open
func (e *Exchange) GetActiveOrders(ctx context.Context, req *order.MultiOrderRequest) (order.FilteredOrders, error) {
err := req.Validate()
if err != nil {
return nil, err
}
var respOrders []GeneralizedOrderResponse
var fPair currency.Pair
for i := range req.Pairs {
fPair, err = e.FormatExchangeCurrency(req.Pairs[i], asset.Spot)
if err != nil {
return nil, err
}
var resp []GeneralizedOrderResponse
resp, err = e.GetOrders(ctx,
[]string{"open", "pending", "active"},
fPair.String())
if err != nil {
return nil, err
}
respOrders = append(respOrders, resp...)
}
format, err := e.GetPairFormat(asset.Spot, false)
if err != nil {
return nil, err
}
orders := make([]order.Detail, len(respOrders))
for i := range respOrders {
var curr currency.Pair
curr, err = currency.NewPairDelimiter(respOrders[i].ProductID,
format.Delimiter)
if err != nil {
return nil, err
}
var side order.Side
side, err = order.StringToOrderSide(respOrders[i].Side)
if err != nil {
return nil, err
}
var orderType order.Type
orderType, err = order.StringToOrderType(respOrders[i].Type)
if err != nil {
log.Errorf(log.ExchangeSys, "%s %v", e.Name, err)
}
orders[i] = order.Detail{
OrderID: respOrders[i].ID,
Amount: respOrders[i].Size,
ExecutedAmount: respOrders[i].FilledSize,
Type: orderType,
Date: respOrders[i].CreatedAt,
Side: side,
Pair: curr,
Exchange: e.Name,
}
}
return req.Filter(e.Name, orders), nil
}
// GetOrderHistory retrieves account order information
// Can Limit response to specific order status
func (e *Exchange) GetOrderHistory(ctx context.Context, req *order.MultiOrderRequest) (order.FilteredOrders, error) {
err := req.Validate()
if err != nil {
return nil, err
}
var respOrders []GeneralizedOrderResponse
if len(req.Pairs) > 0 {
var fPair currency.Pair
var resp []GeneralizedOrderResponse
for i := range req.Pairs {
fPair, err = e.FormatExchangeCurrency(req.Pairs[i], asset.Spot)
if err != nil {
return nil, err
}
resp, err = e.GetOrders(ctx, []string{"done"}, fPair.String())
if err != nil {
return nil, err
}
respOrders = append(respOrders, resp...)
}
} else {
respOrders, err = e.GetOrders(ctx, []string{"done"}, "")
if err != nil {
return nil, err
}
}
format, err := e.GetPairFormat(asset.Spot, false)
if err != nil {
return nil, err
}
orders := make([]order.Detail, len(respOrders))
for i := range respOrders {
var curr currency.Pair
curr, err = currency.NewPairDelimiter(respOrders[i].ProductID,
format.Delimiter)
if err != nil {
return nil, err
}
var side order.Side
side, err = order.StringToOrderSide(respOrders[i].Side)
if err != nil {
return nil, err
}
var orderStatus order.Status
orderStatus, err = order.StringToOrderStatus(respOrders[i].Status)
if err != nil {
log.Errorf(log.ExchangeSys, "%s %v", e.Name, err)
}
var orderType order.Type
orderType, err = order.StringToOrderType(respOrders[i].Type)
if err != nil {
log.Errorf(log.ExchangeSys, "%s %v", e.Name, err)
}
detail := order.Detail{
OrderID: respOrders[i].ID,
Amount: respOrders[i].Size,
ExecutedAmount: respOrders[i].FilledSize,
RemainingAmount: respOrders[i].Size - respOrders[i].FilledSize,
Cost: respOrders[i].ExecutedValue,
CostAsset: curr.Quote,
Type: orderType,
Date: respOrders[i].CreatedAt,
CloseTime: respOrders[i].DoneAt,
Fee: respOrders[i].FillFees,
FeeAsset: curr.Quote,
Side: side,
Status: orderStatus,
Pair: curr,
Price: respOrders[i].Price,
Exchange: e.Name,
}
detail.InferCostsAndTimes()
orders[i] = detail
}
return req.Filter(e.Name, orders), nil
}
// GetHistoricCandles returns a set of candle between two time periods for a
// designated time period
func (e *Exchange) GetHistoricCandles(ctx context.Context, pair currency.Pair, a asset.Item, interval kline.Interval, start, end time.Time) (*kline.Item, error) {
req, err := e.GetKlineRequest(pair, a, interval, start, end, false)
if err != nil {
return nil, err
}
history, err := e.GetHistoricRates(ctx,
req.RequestFormatted.String(),
start.Format(time.RFC3339),
end.Format(time.RFC3339),
int64(req.ExchangeInterval.Duration().Seconds()))
if err != nil {
return nil, err
}
timeSeries := make([]kline.Candle, len(history))
for x := range history {
timeSeries[x] = kline.Candle{
Time: history[x].Time.Time(),
Low: history[x].Low,
High: history[x].High,
Open: history[x].Open,
Close: history[x].Close,
Volume: history[x].Volume,
}
}
return req.ProcessResponse(timeSeries)
}
// GetHistoricCandlesExtended returns candles between a time period for a set time interval
func (e *Exchange) GetHistoricCandlesExtended(ctx context.Context, pair currency.Pair, a asset.Item, interval kline.Interval, start, end time.Time) (*kline.Item, error) {
req, err := e.GetKlineExtendedRequest(pair, a, interval, start, end)
if err != nil {
return nil, err
}
timeSeries := make([]kline.Candle, 0, req.Size())
for x := range req.RangeHolder.Ranges {
var history []History
history, err = e.GetHistoricRates(ctx,
req.RequestFormatted.String(),
req.RangeHolder.Ranges[x].Start.Time.Format(time.RFC3339),
req.RangeHolder.Ranges[x].End.Time.Format(time.RFC3339),
int64(req.ExchangeInterval.Duration().Seconds()))
if err != nil {
return nil, err
}
for i := range history {
timeSeries = append(timeSeries, kline.Candle{
Time: history[i].Time.Time(),
Low: history[i].Low,
High: history[i].High,
Open: history[i].Open,
Close: history[i].Close,
Volume: history[i].Volume,
})
}
}
return req.ProcessResponse(timeSeries)
}
// ValidateAPICredentials validates current credentials used for wrapper
// functionality
func (e *Exchange) ValidateAPICredentials(ctx context.Context, assetType asset.Item) error {
_, err := e.UpdateAccountInfo(ctx, assetType)
return e.CheckTransientError(err)
}
// GetServerTime returns the current exchange server time.
func (e *Exchange) GetServerTime(ctx context.Context, _ asset.Item) (time.Time, error) {
st, err := e.GetCurrentServerTime(ctx)
if err != nil {
return time.Time{}, err
}
return st.ISO, nil
}
// GetLatestFundingRates returns the latest funding rates data
func (e *Exchange) GetLatestFundingRates(context.Context, *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) {
return nil, common.ErrFunctionNotSupported
}
// GetFuturesContractDetails returns all contracts from the exchange by asset type
func (e *Exchange) GetFuturesContractDetails(context.Context, asset.Item) ([]futures.Contract, error) {
return nil, common.ErrFunctionNotSupported
}
// UpdateOrderExecutionLimits updates order execution limits
func (e *Exchange) UpdateOrderExecutionLimits(_ context.Context, _ asset.Item) error {
return common.ErrNotYetImplemented
}
// GetCurrencyTradeURL returns the URL to the exchange's trade page for the given asset and currency pair
func (e *Exchange) GetCurrencyTradeURL(_ context.Context, a asset.Item, cp currency.Pair) (string, error) {
_, err := e.CurrencyPairs.IsPairEnabled(cp, a)
if err != nil {
return "", err
}
cp.Delimiter = currency.DashDelimiter
return tradeBaseURL + cp.Upper().String(), nil
}

View File

@@ -1,22 +0,0 @@
package coinbasepro
import (
"time"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
)
// Coinbasepro rate limit conts
const (
coinbaseproRateInterval = time.Second
coinbaseproAuthRate = 5
coinbaseproUnauthRate = 2
)
// GetRateLimit returns the rate limit for the exchange
func GetRateLimit() request.RateLimitDefinitions {
return request.RateLimitDefinitions{
request.Auth: request.NewRateLimitWithWeight(coinbaseproRateInterval, coinbaseproAuthRate, 1),
request.UnAuth: request.NewRateLimitWithWeight(coinbaseproRateInterval, coinbaseproUnauthRate, 1),
}
}

View File

@@ -53,8 +53,12 @@ const (
DefaultWebsocketOrderbookBufferLimit = 5 DefaultWebsocketOrderbookBufferLimit = 5
) )
// ErrSymbolCannotBeMatched returned on symbol matching failure // Public Errors
var ErrSymbolCannotBeMatched = errors.New("symbol cannot be matched") var (
ErrSettingProxyAddress = errors.New("error setting proxy address")
ErrEndpointPathNotFound = errors.New("no endpoint path found for the given key")
ErrSymbolNotMatched = errors.New("symbol cannot be matched")
)
var ( var (
errEndpointStringNotFound = errors.New("endpoint string not found") errEndpointStringNotFound = errors.New("endpoint string not found")
@@ -81,8 +85,7 @@ func (b *Base) SetClientProxyAddress(addr string) error {
} }
proxy, err := url.Parse(addr) proxy, err := url.Parse(addr)
if err != nil { if err != nil {
return fmt.Errorf("setting proxy address error %s", return fmt.Errorf("%w %w", ErrSettingProxyAddress, err)
err)
} }
err = b.Requester.SetProxy(proxy) err = b.Requester.SetProxy(proxy)
@@ -204,7 +207,7 @@ func (b *Base) GetLastPairsUpdateTime() int64 {
return b.CurrencyPairs.LastUpdated return b.CurrencyPairs.LastUpdated
} }
// GetAssetTypes returns the either the enabled or available asset types for an // GetAssetTypes returns either the enabled or available asset types for an
// individual exchange // individual exchange
func (b *Base) GetAssetTypes(enabled bool) asset.Items { func (b *Base) GetAssetTypes(enabled bool) asset.Items {
return b.CurrencyPairs.GetAssetTypes(enabled) return b.CurrencyPairs.GetAssetTypes(enabled)
@@ -251,7 +254,7 @@ func (b *Base) GetPairAndAssetTypeRequestFormatted(symbol string) (currency.Pair
} }
} }
} }
return currency.EMPTYPAIR, asset.Empty, ErrSymbolCannotBeMatched return currency.EMPTYPAIR, asset.Empty, ErrSymbolNotMatched
} }
// GetClientBankAccounts returns banking details associated with // GetClientBankAccounts returns banking details associated with
@@ -696,6 +699,10 @@ func (b *Base) UpdatePairs(incoming currency.Pairs, a asset.Item, enabled, force
diff.Remove) diff.Remove)
} }
} }
err = common.NilGuard(b.Config, b.Config.CurrencyPairs)
if err != nil {
return err
}
err = b.Config.CurrencyPairs.StorePairs(a, incoming, enabled) err = b.Config.CurrencyPairs.StorePairs(a, incoming, enabled)
if err != nil { if err != nil {
return err return err
@@ -1278,7 +1285,7 @@ func (e *Endpoints) GetURL(endpoint URL) (string, error) {
defer e.mu.RUnlock() defer e.mu.RUnlock()
val, ok := e.defaults[endpoint.String()] val, ok := e.defaults[endpoint.String()]
if !ok { if !ok {
return "", fmt.Errorf("no endpoint path found for the given key: %v", endpoint) return "", fmt.Errorf("%w %v", ErrEndpointPathNotFound, endpoint)
} }
return val, nil return val, nil
} }

View File

@@ -1998,10 +1998,10 @@ func TestGetPairAndAssetTypeRequestFormatted(t *testing.T) {
require.ErrorIs(t, err, currency.ErrCurrencyPairEmpty) require.ErrorIs(t, err, currency.ErrCurrencyPairEmpty)
_, _, err = b.GetPairAndAssetTypeRequestFormatted("BTCAUD") _, _, err = b.GetPairAndAssetTypeRequestFormatted("BTCAUD")
require.ErrorIs(t, err, ErrSymbolCannotBeMatched) require.ErrorIs(t, err, ErrSymbolNotMatched)
_, _, err = b.GetPairAndAssetTypeRequestFormatted("BTCUSDT") _, _, err = b.GetPairAndAssetTypeRequestFormatted("BTCUSDT")
require.ErrorIs(t, err, ErrSymbolCannotBeMatched) require.ErrorIs(t, err, ErrSymbolNotMatched)
p, a, err := b.GetPairAndAssetTypeRequestFormatted("BTC-USDT") p, a, err := b.GetPairAndAssetTypeRequestFormatted("BTC-USDT")
require.NoError(t, err) require.NoError(t, err)

View File

@@ -245,7 +245,7 @@ func SetupMultiPositionTracker(setup *MultiPositionTrackerSetup) (*MultiPosition
return nil, errNilSetup return nil, errNilSetup
} }
if setup.Exchange == "" { if setup.Exchange == "" {
return nil, errExchangeNameEmpty return nil, common.ErrExchangeNameNotSet
} }
var err error var err error
setup.Exchange, err = checkTrackerPrerequisitesLowerExchange(setup.Exchange, setup.Asset, setup.Pair) setup.Exchange, err = checkTrackerPrerequisitesLowerExchange(setup.Exchange, setup.Asset, setup.Pair)
@@ -1076,7 +1076,7 @@ func CheckFundingRatePrerequisites(getFundingData, includePredicted, includePaym
// checkTrackerPrerequisitesLowerExchange is a common set of checks for futures position tracking // checkTrackerPrerequisitesLowerExchange is a common set of checks for futures position tracking
func checkTrackerPrerequisitesLowerExchange(exch string, item asset.Item, cp currency.Pair) (string, error) { func checkTrackerPrerequisitesLowerExchange(exch string, item asset.Item, cp currency.Pair) (string, error) {
if exch == "" { if exch == "" {
return "", errExchangeNameEmpty return "", common.ErrExchangeNameNotSet
} }
exch = strings.ToLower(exch) exch = strings.ToLower(exch)
if !item.IsFutures() { if !item.IsFutures() {

View File

@@ -88,7 +88,7 @@ func TestTrackNewOrder(t *testing.T) {
assert.ErrorIs(t, err, common.ErrNilPointer) assert.ErrorIs(t, err, common.ErrNilPointer)
err = c.TrackNewOrder(&order.Detail{}, false) err = c.TrackNewOrder(&order.Detail{}, false)
assert.ErrorIs(t, err, errExchangeNameEmpty) assert.ErrorIs(t, err, common.ErrExchangeNameNotSet)
od := &order.Detail{ od := &order.Detail{
Exchange: exch, Exchange: exch,
@@ -206,7 +206,7 @@ func TestSetupMultiPositionTracker(t *testing.T) {
setup := &MultiPositionTrackerSetup{} setup := &MultiPositionTrackerSetup{}
_, err = SetupMultiPositionTracker(setup) _, err = SetupMultiPositionTracker(setup)
assert.ErrorIs(t, err, errExchangeNameEmpty) assert.ErrorIs(t, err, common.ErrExchangeNameNotSet)
setup.Exchange = testExchange setup.Exchange = testExchange
_, err = SetupMultiPositionTracker(setup) _, err = SetupMultiPositionTracker(setup)
@@ -249,7 +249,7 @@ func TestMultiPositionTrackerTrackNewOrder(t *testing.T) {
ExchangePNLCalculation: &FakePNL{}, ExchangePNLCalculation: &FakePNL{},
} }
_, err := SetupMultiPositionTracker(setup) _, err := SetupMultiPositionTracker(setup)
assert.ErrorIs(t, err, errExchangeNameEmpty) assert.ErrorIs(t, err, common.ErrExchangeNameNotSet)
setup.Exchange = testExchange setup.Exchange = testExchange
resp, err := SetupMultiPositionTracker(setup) resp, err := SetupMultiPositionTracker(setup)
@@ -264,7 +264,7 @@ func TestMultiPositionTrackerTrackNewOrder(t *testing.T) {
OrderID: "1", OrderID: "1",
Amount: 1, Amount: 1,
}) })
assert.ErrorIs(t, err, errExchangeNameEmpty) assert.ErrorIs(t, err, common.ErrExchangeNameNotSet)
err = resp.TrackNewOrder(&order.Detail{ err = resp.TrackNewOrder(&order.Detail{
Date: tt, Date: tt,
@@ -417,7 +417,7 @@ func TestPositionControllerTestTrackNewOrder(t *testing.T) {
Side: order.Long, Side: order.Long,
OrderID: "lol", OrderID: "lol",
}) })
assert.ErrorIs(t, err, errExchangeNameEmpty) assert.ErrorIs(t, err, common.ErrExchangeNameNotSet)
err = pc.TrackNewOrder(&order.Detail{ err = pc.TrackNewOrder(&order.Detail{
Exchange: testExchange, Exchange: testExchange,
@@ -524,7 +524,7 @@ func TestGetPositionsForExchange(t *testing.T) {
p := currency.NewBTCUSDT() p := currency.NewBTCUSDT()
_, err := c.GetPositionsForExchange("", asset.Futures, p) _, err := c.GetPositionsForExchange("", asset.Futures, p)
assert.ErrorIs(t, err, errExchangeNameEmpty) assert.ErrorIs(t, err, common.ErrExchangeNameNotSet)
pos, err := c.GetPositionsForExchange(testExchange, asset.Futures, p) pos, err := c.GetPositionsForExchange(testExchange, asset.Futures, p)
assert.ErrorIs(t, err, ErrPositionNotFound) assert.ErrorIs(t, err, ErrPositionNotFound)
@@ -581,7 +581,7 @@ func TestClearPositionsForExchange(t *testing.T) {
c := &PositionController{} c := &PositionController{}
p := currency.NewBTCUSDT() p := currency.NewBTCUSDT()
err := c.ClearPositionsForExchange("", asset.Futures, p) err := c.ClearPositionsForExchange("", asset.Futures, p)
assert.ErrorIs(t, err, errExchangeNameEmpty) assert.ErrorIs(t, err, common.ErrExchangeNameNotSet)
err = c.ClearPositionsForExchange(testExchange, asset.Futures, p) err = c.ClearPositionsForExchange(testExchange, asset.Futures, p)
assert.ErrorIs(t, err, ErrPositionNotFound) assert.ErrorIs(t, err, ErrPositionNotFound)
@@ -656,7 +656,7 @@ func TestSetupPositionTracker(t *testing.T) {
p, err = SetupPositionTracker(&PositionTrackerSetup{ p, err = SetupPositionTracker(&PositionTrackerSetup{
Asset: asset.Spot, Asset: asset.Spot,
}) })
assert.ErrorIs(t, err, errExchangeNameEmpty) assert.ErrorIs(t, err, common.ErrExchangeNameNotSet)
if p != nil { if p != nil {
t.Error("expected nil") t.Error("expected nil")
@@ -758,7 +758,7 @@ func TestUpdateOpenPositionUnrealisedPNL(t *testing.T) {
pc := SetupPositionController() pc := SetupPositionController()
_, err := pc.UpdateOpenPositionUnrealisedPNL("", asset.Futures, currency.NewBTCUSDT(), 2, time.Now()) _, err := pc.UpdateOpenPositionUnrealisedPNL("", asset.Futures, currency.NewBTCUSDT(), 2, time.Now())
assert.ErrorIs(t, err, errExchangeNameEmpty) assert.ErrorIs(t, err, common.ErrExchangeNameNotSet)
_, err = pc.UpdateOpenPositionUnrealisedPNL("hi", asset.Futures, currency.NewBTCUSDT(), 2, time.Now()) _, err = pc.UpdateOpenPositionUnrealisedPNL("hi", asset.Futures, currency.NewBTCUSDT(), 2, time.Now())
assert.ErrorIs(t, err, ErrPositionNotFound) assert.ErrorIs(t, err, ErrPositionNotFound)
@@ -803,7 +803,7 @@ func TestSetCollateralCurrency(t *testing.T) {
t.Parallel() t.Parallel()
pc := SetupPositionController() pc := SetupPositionController()
err := pc.SetCollateralCurrency("", asset.Spot, currency.EMPTYPAIR, currency.Code{}) err := pc.SetCollateralCurrency("", asset.Spot, currency.EMPTYPAIR, currency.Code{})
assert.ErrorIs(t, err, errExchangeNameEmpty) assert.ErrorIs(t, err, common.ErrExchangeNameNotSet)
err = pc.SetCollateralCurrency("hi", asset.Spot, currency.EMPTYPAIR, currency.Code{}) err = pc.SetCollateralCurrency("hi", asset.Spot, currency.EMPTYPAIR, currency.Code{})
assert.ErrorIs(t, err, ErrNotFuturesAsset) assert.ErrorIs(t, err, ErrNotFuturesAsset)
@@ -905,7 +905,7 @@ func TestMPTLiquidate(t *testing.T) {
Asset: item, Asset: item,
} }
_, err = SetupPositionTracker(setup) _, err = SetupPositionTracker(setup)
assert.ErrorIs(t, err, errExchangeNameEmpty) assert.ErrorIs(t, err, common.ErrExchangeNameNotSet)
setup.Exchange = "exch" setup.Exchange = "exch"
_, err = SetupPositionTracker(setup) _, err = SetupPositionTracker(setup)
@@ -995,7 +995,7 @@ func TestGetOpenPosition(t *testing.T) {
tn := time.Now() tn := time.Now()
_, err := pc.GetOpenPosition("", asset.Futures, cp) _, err := pc.GetOpenPosition("", asset.Futures, cp)
assert.ErrorIs(t, err, errExchangeNameEmpty) assert.ErrorIs(t, err, common.ErrExchangeNameNotSet)
_, err = pc.GetOpenPosition(testExchange, asset.Futures, cp) _, err = pc.GetOpenPosition(testExchange, asset.Futures, cp)
assert.ErrorIs(t, err, ErrPositionNotFound) assert.ErrorIs(t, err, ErrPositionNotFound)
@@ -1053,7 +1053,7 @@ func TestPCTrackFundingDetails(t *testing.T) {
Pair: p, Pair: p,
} }
err = pc.TrackFundingDetails(rates) err = pc.TrackFundingDetails(rates)
assert.ErrorIs(t, err, errExchangeNameEmpty) assert.ErrorIs(t, err, common.ErrExchangeNameNotSet)
rates.Exchange = testExchange rates.Exchange = testExchange
err = pc.TrackFundingDetails(rates) err = pc.TrackFundingDetails(rates)
@@ -1104,7 +1104,7 @@ func TestMPTTrackFundingDetails(t *testing.T) {
Pair: cp, Pair: cp,
} }
err = mpt.TrackFundingDetails(rates) err = mpt.TrackFundingDetails(rates)
assert.ErrorIs(t, err, errExchangeNameEmpty) assert.ErrorIs(t, err, common.ErrExchangeNameNotSet)
mpt.exchange = testExchange mpt.exchange = testExchange
rates = &fundingrate.HistoricalRates{ rates = &fundingrate.HistoricalRates{
@@ -1206,7 +1206,7 @@ func TestPTTrackFundingDetails(t *testing.T) {
rates.Exchange = "" rates.Exchange = ""
err = p.TrackFundingDetails(rates) err = p.TrackFundingDetails(rates)
assert.ErrorIs(t, err, errExchangeNameEmpty) assert.ErrorIs(t, err, common.ErrExchangeNameNotSet)
p = nil p = nil
err = p.TrackFundingDetails(rates) err = p.TrackFundingDetails(rates)
@@ -1278,7 +1278,7 @@ func TestGetCurrencyForRealisedPNL(t *testing.T) {
func TestCheckTrackerPrerequisitesLowerExchange(t *testing.T) { func TestCheckTrackerPrerequisitesLowerExchange(t *testing.T) {
t.Parallel() t.Parallel()
_, err := checkTrackerPrerequisitesLowerExchange("", asset.Spot, currency.EMPTYPAIR) _, err := checkTrackerPrerequisitesLowerExchange("", asset.Spot, currency.EMPTYPAIR)
assert.ErrorIs(t, err, errExchangeNameEmpty) assert.ErrorIs(t, err, common.ErrExchangeNameNotSet)
upperExch := "IM UPPERCASE" upperExch := "IM UPPERCASE"
_, err = checkTrackerPrerequisitesLowerExchange(upperExch, asset.Spot, currency.EMPTYPAIR) _, err = checkTrackerPrerequisitesLowerExchange(upperExch, asset.Spot, currency.EMPTYPAIR)

View File

@@ -42,7 +42,6 @@ var (
// ErrOrderHistoryTooLarge is returned when you lookup order history, but with too early a start date // ErrOrderHistoryTooLarge is returned when you lookup order history, but with too early a start date
ErrOrderHistoryTooLarge = errors.New("order history start date too long ago") ErrOrderHistoryTooLarge = errors.New("order history start date too long ago")
errExchangeNameEmpty = errors.New("exchange name empty")
errExchangeNameMismatch = errors.New("exchange name mismatch") errExchangeNameMismatch = errors.New("exchange name mismatch")
errTimeUnset = errors.New("time unset") errTimeUnset = errors.New("time unset")
errMissingPNLCalculationFunctions = errors.New("futures tracker requires exchange PNL calculation functions") errMissingPNLCalculationFunctions = errors.New("futures tracker requires exchange PNL calculation functions")

View File

@@ -38,7 +38,7 @@ func TestSubmitValidate(t *testing.T) {
Submit: nil, Submit: nil,
}, // nil struct }, // nil struct
{ {
ExpectedErr: errExchangeNameUnset, ExpectedErr: common.ErrExchangeNameNotSet,
Submit: &Submit{}, Submit: &Submit{},
}, // empty exchange }, // empty exchange
{ {
@@ -831,7 +831,7 @@ func TestStringToOrderType(t *testing.T) {
{"mMp", MarketMakerProtection, nil}, {"mMp", MarketMakerProtection, nil},
{"tWaP", TWAP, nil}, {"tWaP", TWAP, nil},
{"TWAP", TWAP, nil}, {"TWAP", TWAP, nil},
{"woahMan", UnknownType, errUnrecognisedOrderType}, {"woahMan", UnknownType, ErrUnrecognisedOrderType},
{"chase", Chase, nil}, {"chase", Chase, nil},
{"MOVE_ORDER_STOP", TrailingStop, nil}, {"MOVE_ORDER_STOP", TrailingStop, nil},
{"mOVe_OrdeR_StoP", TrailingStop, nil}, {"mOVe_OrdeR_StoP", TrailingStop, nil},
@@ -842,6 +842,8 @@ func TestStringToOrderType(t *testing.T) {
{"Take ProfIt", TakeProfit, nil}, {"Take ProfIt", TakeProfit, nil},
{"TAKE PROFIT MARkEt", TakeProfitMarket, nil}, {"TAKE PROFIT MARkEt", TakeProfitMarket, nil},
{"TAKE_PROFIT_MARkEt", TakeProfitMarket, nil}, {"TAKE_PROFIT_MARkEt", TakeProfitMarket, nil},
{"brAcket", Bracket, nil},
{"TRIGGER_bracket", Bracket, nil},
{"optimal_limit", OptimalLimit, nil}, {"optimal_limit", OptimalLimit, nil},
{"OPTIMAL_LIMIT", OptimalLimit, nil}, {"OPTIMAL_LIMIT", OptimalLimit, nil},
} }
@@ -1135,7 +1137,7 @@ func TestValidationOnOrderTypes(t *testing.T) {
getOrders.Side = AnySide getOrders.Side = AnySide
err = getOrders.Validate() err = getOrders.Validate()
require.ErrorIs(t, err, errUnrecognisedOrderType) require.ErrorIs(t, err, ErrUnrecognisedOrderType)
errTestError := errors.New("test error") errTestError := errors.New("test error")
getOrders.Type = AnyType getOrders.Type = AnyType
@@ -1743,6 +1745,6 @@ func TestMarshalOrder(t *testing.T) {
} }
j, err := json.Marshal(orderSubmit) j, err := json.Marshal(orderSubmit)
require.NoError(t, err, "Marshal must not error") require.NoError(t, err, "Marshal must not error")
exp := []byte(`{"Exchange":"test","Type":4,"Side":"BUY","Pair":"BTC-USDT","AssetType":"spot","TimeInForce":"","ReduceOnly":false,"Leverage":0,"Price":1000,"Amount":1,"QuoteAmount":0,"TriggerPrice":0,"TriggerPriceType":0,"ClientID":"","ClientOrderID":"","AutoBorrow":false,"MarginType":"multi","RetrieveFees":false,"RetrieveFeeDelay":0,"RiskManagementModes":{"Mode":"","TakeProfit":{"Enabled":false,"TriggerPriceType":0,"Price":0,"LimitPrice":0,"OrderType":0},"StopLoss":{"Enabled":false,"TriggerPriceType":0,"Price":0,"LimitPrice":0,"OrderType":0},"StopEntry":{"Enabled":false,"TriggerPriceType":0,"Price":0,"LimitPrice":0,"OrderType":0}},"Hidden":false,"Iceberg":false,"TrackingMode":0,"TrackingValue":0}`) exp := []byte(`{"Exchange":"test","Type":4,"Side":"BUY","Pair":"BTC-USDT","AssetType":"spot","TimeInForce":"","ReduceOnly":false,"Leverage":0,"Price":1000,"Amount":1,"QuoteAmount":0,"TriggerPrice":0,"TriggerPriceType":0,"ClientID":"","ClientOrderID":"","AutoBorrow":false,"MarginType":"multi","RetrieveFees":false,"RetrieveFeeDelay":0,"RiskManagementModes":{"Mode":"","TakeProfit":{"Enabled":false,"TriggerPriceType":0,"Price":0,"LimitPrice":0,"OrderType":0},"StopLoss":{"Enabled":false,"TriggerPriceType":0,"Price":0,"LimitPrice":0,"OrderType":0},"StopEntry":{"Enabled":false,"TriggerPriceType":0,"Price":0,"LimitPrice":0,"OrderType":0}},"Hidden":false,"Iceberg":false,"EndTime":"0001-01-01T00:00:00Z","StopDirection":false,"TrackingMode":0,"TrackingValue":0,"RFQDisabled":false}`)
assert.Equal(t, exp, j) assert.Equal(t, exp, j)
} }

View File

@@ -28,6 +28,7 @@ var (
ErrSubmitLeverageNotSupported = errors.New("leverage is not supported via order submission") ErrSubmitLeverageNotSupported = errors.New("leverage is not supported via order submission")
ErrClientOrderIDNotSupported = errors.New("client order id not supported") ErrClientOrderIDNotSupported = errors.New("client order id not supported")
ErrUnsupportedOrderType = errors.New("unsupported order type") ErrUnsupportedOrderType = errors.New("unsupported order type")
ErrUnsupportedStatusType = errors.New("unsupported status type")
// ErrNoRates is returned when no margin rates are returned when they are expected // ErrNoRates is returned when no margin rates are returned when they are expected
ErrNoRates = errors.New("no rates") ErrNoRates = errors.New("no rates")
ErrCannotLiquidate = errors.New("cannot liquidate position") ErrCannotLiquidate = errors.New("cannot liquidate position")
@@ -93,10 +94,18 @@ type Submit struct {
// Iceberg specifies whether or not only visible portions of orders are shown in iceberg orders // Iceberg specifies whether or not only visible portions of orders are shown in iceberg orders
Iceberg bool Iceberg bool
// EndTime is the moment which a good til date order is valid until
EndTime time.Time
// StopDirection is the direction from which the stop order will trigger
StopDirection StopDirection
// TrackingMode specifies the way trailing stop and chase orders follow the market price or ask/bid prices. // TrackingMode specifies the way trailing stop and chase orders follow the market price or ask/bid prices.
// See: https://www.okx.com/docs-v5/en/#order-book-trading-algo-trading-post-place-algo-order // See: https://www.okx.com/docs-v5/en/#order-book-trading-algo-trading-post-place-algo-order
TrackingMode TrackingMode TrackingMode TrackingMode
TrackingValue float64 TrackingValue float64
// RFQDisabled, when set, attempts to route the order to the exchange CLOB. Currently only supported by Coinbase
RFQDisabled bool
} }
// SubmitResponse is what is returned after submitting an order to an exchange // SubmitResponse is what is returned after submitting an order to an exchange
@@ -375,6 +384,7 @@ const (
StopLimit = Stop | Limit StopLimit = Stop | Limit
StopMarket = Stop | Market StopMarket = Stop | Market
TakeProfitMarket = TakeProfit | Market TakeProfitMarket = TakeProfit | Market
Bracket = Stop | TakeProfit
) )
// order-type string representations // order-type string representations
@@ -396,9 +406,31 @@ const (
orderOCO = "OCO" orderOCO = "OCO"
orderOptimalLimit = "OPTIMAL_LIMIT" orderOptimalLimit = "OPTIMAL_LIMIT"
orderMarketMakerProtection = "MMP" orderMarketMakerProtection = "MMP"
orderBracket = "BRACKET"
orderAnyType = "ANY" orderAnyType = "ANY"
) )
// AllOrderTypes collects all order types for easy and consistent comparisons
var AllOrderTypes = Limit |
Market |
Stop |
StopLimit |
StopMarket |
TakeProfit |
TakeProfitMarket |
TrailingStop |
IOS |
AnyType |
Liquidation |
Trigger |
OCO |
ConditionalStop |
TWAP |
Chase |
OptimalLimit |
MarketMakerProtection |
Bracket
// Side enforces a standard for order sides across the code base // Side enforces a standard for order sides across the code base
type Side uint32 type Side uint32
@@ -453,6 +485,17 @@ type ClassificationError struct {
// MultiOrderRequest. // MultiOrderRequest.
type FilteredOrders []Detail type FilteredOrders []Detail
// StopDirection is the direction from which the stop order will trigger; Up will have the order trigger
// when the last trade price goes above the TriggerPrice; Down will have the order trigger when the
// last trade price goes below the TriggerPrice
type StopDirection bool
// StopDirection types
const (
StopUp StopDirection = true
StopDown StopDirection = false
)
// RiskManagement represents a risk management detail information. // RiskManagement represents a risk management detail information.
type RiskManagement struct { type RiskManagement struct {
Enabled bool Enabled bool

View File

@@ -40,12 +40,11 @@ var (
ErrAmountMustBeSet = errors.New("amount must be set") ErrAmountMustBeSet = errors.New("amount must be set")
ErrClientOrderIDMustBeSet = errors.New("client order ID must be set") ErrClientOrderIDMustBeSet = errors.New("client order ID must be set")
ErrUnknownSubmissionAmountType = errors.New("unknown submission amount type") ErrUnknownSubmissionAmountType = errors.New("unknown submission amount type")
ErrUnrecognisedOrderType = errors.New("unrecognised order type")
) )
var ( var (
errUnrecognisedOrderType = errors.New("unrecognised order type")
errUnrecognisedOrderStatus = errors.New("unrecognised order status") errUnrecognisedOrderStatus = errors.New("unrecognised order status")
errExchangeNameUnset = errors.New("exchange name unset")
errOrderSubmitIsNil = errors.New("order submit is nil") errOrderSubmitIsNil = errors.New("order submit is nil")
errOrderSubmitResponseIsNil = errors.New("order submit response is nil") errOrderSubmitResponseIsNil = errors.New("order submit response is nil")
errOrderDetailIsNil = errors.New("order detail is nil") errOrderDetailIsNil = errors.New("order detail is nil")
@@ -64,7 +63,7 @@ func (s *Submit) Validate(requirements protocol.TradingRequirements, opt ...vali
} }
if s.Exchange == "" { if s.Exchange == "" {
return errExchangeNameUnset return common.ErrExchangeNameNotSet
} }
if s.Pair.IsEmpty() { if s.Pair.IsEmpty() {
@@ -83,7 +82,7 @@ func (s *Submit) Validate(requirements protocol.TradingRequirements, opt ...vali
return fmt.Errorf("%w %v", ErrSideIsInvalid, s.Side) return fmt.Errorf("%w %v", ErrSideIsInvalid, s.Side)
} }
if s.Type != Market && s.Type != Limit { if AllOrderTypes&s.Type != s.Type || s.Type == UnknownType {
return ErrTypeIsInvalid return ErrTypeIsInvalid
} }
@@ -697,6 +696,8 @@ func (t Type) String() string {
return orderTrigger return orderTrigger
case OCO: case OCO:
return orderOCO return orderOCO
case Bracket:
return orderBracket
case OptimalLimit: case OptimalLimit:
return orderOptimalLimit return orderOptimalLimit
case MarketMakerProtection: case MarketMakerProtection:
@@ -1136,8 +1137,10 @@ func StringToOrderType(oType string) (Type, error) {
return TakeProfit, nil return TakeProfit, nil
case orderLiquidation: case orderLiquidation:
return Liquidation, nil return Liquidation, nil
case orderBracket, "TRIGGER_BRACKET":
return Bracket, nil
default: default:
return UnknownType, fmt.Errorf("'%v' %w", oType, errUnrecognisedOrderType) return UnknownType, fmt.Errorf("'%v' %w", oType, ErrUnrecognisedOrderType)
} }
} }
@@ -1263,7 +1266,7 @@ func (g *MultiOrderRequest) Validate(opt ...validate.Checker) error {
} }
if g.Type == UnknownType { if g.Type == UnknownType {
return errUnrecognisedOrderType return ErrUnrecognisedOrderType
} }
var errs error var errs error

View File

@@ -13,7 +13,7 @@ var (
) )
// TimeInForce enforces a standard for time-in-force values across the code base. // TimeInForce enforces a standard for time-in-force values across the code base.
type TimeInForce uint8 type TimeInForce uint16
// TimeInForce types // TimeInForce types
const ( const (
@@ -25,8 +25,9 @@ const (
FillOrKill FillOrKill
ImmediateOrCancel ImmediateOrCancel
PostOnly PostOnly
StopOrReduce
supportedTimeInForceFlag = GoodTillCancel | GoodTillDay | GoodTillTime | GoodTillCrossing | FillOrKill | ImmediateOrCancel | PostOnly supportedTimeInForceFlag = GoodTillCancel | GoodTillDay | GoodTillTime | GoodTillCrossing | FillOrKill | ImmediateOrCancel | PostOnly | StopOrReduce
) )
// time-in-force string representations // time-in-force string representations
@@ -38,6 +39,7 @@ const (
fokStr = "FOK" fokStr = "FOK"
iocStr = "IOC" iocStr = "IOC"
postonlyStr = "POSTONLY" postonlyStr = "POSTONLY"
sorStr = "SOR"
) )
// Is checks to see if the enum contains the flag // Is checks to see if the enum contains the flag
@@ -64,6 +66,8 @@ func StringToTimeInForce(timeInForce string) (TimeInForce, error) {
result = FillOrKill result = FillOrKill
case "POC", "POST_ONLY", "PENDINGORCANCEL", postonlyStr: case "POC", "POST_ONLY", "PENDINGORCANCEL", postonlyStr:
result = PostOnly result = PostOnly
case "STOPORREDUCE", "STOP_OR_REDUCE", sorStr:
result = StopOrReduce
} }
if result == UnknownTIF && timeInForce != "" { if result == UnknownTIF && timeInForce != "" {
return UnknownTIF, fmt.Errorf("%w: tif=%s", ErrInvalidTimeInForce, timeInForce) return UnknownTIF, fmt.Errorf("%w: tif=%s", ErrInvalidTimeInForce, timeInForce)
@@ -111,6 +115,9 @@ func (t TimeInForce) String() string {
if t.Is(PostOnly) { if t.Is(PostOnly) {
tifStrings = append(tifStrings, postonlyStr) tifStrings = append(tifStrings, postonlyStr)
} }
if t.Is(StopOrReduce) {
tifStrings = append(tifStrings, sorStr)
}
if len(tifStrings) == 0 { if len(tifStrings) == 0 {
return "UNKNOWN" return "UNKNOWN"
} }

View File

@@ -22,6 +22,7 @@ func TestTimeInForceIs(t *testing.T) {
FillOrKill: {FillOrKill}, FillOrKill: {FillOrKill},
PostOnly: {PostOnly}, PostOnly: {PostOnly},
GoodTillCrossing: {GoodTillCrossing}, GoodTillCrossing: {GoodTillCrossing},
StopOrReduce: {StopOrReduce},
} }
for tif, exps := range tifValuesMap { for tif, exps := range tifValuesMap {
for _, v := range exps { for _, v := range exps {
@@ -49,6 +50,7 @@ func TestIsValid(t *testing.T) {
GoodTillDay | PostOnly: true, GoodTillDay | PostOnly: true,
GoodTillCrossing | PostOnly: true, GoodTillCrossing | PostOnly: true,
GoodTillCancel | PostOnly: true, GoodTillCancel | PostOnly: true,
StopOrReduce: true,
UnknownTIF: true, UnknownTIF: true,
} }
for tif, value := range timeInForceValidityMap { for tif, value := range timeInForceValidityMap {
@@ -80,6 +82,8 @@ var timeInForceStringToValueMap = map[string]struct {
"GTX": {TIF: GoodTillCrossing}, "GTX": {TIF: GoodTillCrossing},
"GOOD_TILL_CROSSING": {TIF: GoodTillCrossing}, "GOOD_TILL_CROSSING": {TIF: GoodTillCrossing},
"Good Til crossing": {TIF: GoodTillCrossing}, "Good Til crossing": {TIF: GoodTillCrossing},
"sor": {TIF: StopOrReduce},
"STOP_OR_REDUCE": {TIF: StopOrReduce},
"abcdfeg": {TIF: UnknownTIF, Error: ErrInvalidTimeInForce}, "abcdfeg": {TIF: UnknownTIF, Error: ErrInvalidTimeInForce},
} }
@@ -113,6 +117,7 @@ func TestString(t *testing.T) {
GoodTillTime | PostOnly: "GTT,POSTONLY", GoodTillTime | PostOnly: "GTT,POSTONLY",
GoodTillDay | PostOnly: "GTD,POSTONLY", GoodTillDay | PostOnly: "GTD,POSTONLY",
FillOrKill | ImmediateOrCancel: "IOC,FOK", FillOrKill | ImmediateOrCancel: "IOC,FOK",
StopOrReduce: "SOR",
TimeInForce(1): "UNKNOWN", TimeInForce(1): "UNKNOWN",
} }
for x := range valMap { for x := range valMap {
@@ -125,9 +130,9 @@ func TestUnmarshalJSON(t *testing.T) {
t.Parallel() t.Parallel()
targets := []TimeInForce{ targets := []TimeInForce{
GoodTillCancel | PostOnly | ImmediateOrCancel, GoodTillCancel | PostOnly, GoodTillCancel, UnknownTIF, PostOnly | ImmediateOrCancel, GoodTillCancel | PostOnly | ImmediateOrCancel, GoodTillCancel | PostOnly, GoodTillCancel, UnknownTIF, PostOnly | ImmediateOrCancel,
GoodTillCancel, GoodTillCancel, PostOnly, PostOnly, ImmediateOrCancel, GoodTillDay, GoodTillDay, GoodTillTime, FillOrKill, FillOrKill, GoodTillCancel, GoodTillCancel, PostOnly, PostOnly, ImmediateOrCancel, GoodTillDay, GoodTillDay, GoodTillTime, FillOrKill, FillOrKill, StopOrReduce,
} }
data := []byte(`{"tifs": ["GTC,POSTONLY,IOC", "GTC,POSTONLY", "GTC", "", "POSTONLY,IOC", "GoodTilCancel", "GoodTILLCANCEL", "POST_ONLY", "POC","IOC", "GTD", "gtd","gtt", "fok", "fillOrKill"]}`) data := []byte(`{"tifs": ["GTC,POSTONLY,IOC", "GTC,POSTONLY", "GTC", "", "POSTONLY,IOC", "GoodTilCancel", "GoodTILLCANCEL", "POST_ONLY", "POC","IOC", "GTD", "gtd","gtt", "fok", "fillOrKill", "SOR"]}`)
target := &struct { target := &struct {
TIFs []TimeInForce `json:"tifs"` TIFs []TimeInForce `json:"tifs"`
}{} }{}
@@ -135,7 +140,7 @@ func TestUnmarshalJSON(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, targets, target.TIFs) require.Equal(t, targets, target.TIFs)
data = []byte(`{"tifs": ["abcd,POSTONLY,IOC", "GTC,POSTONLY", "GTC", "", "POSTONLY,IOC", "GoodTilCancel", "GoodTILLCANCEL", "POST_ONLY", "POC","IOC", "GTD", "gtd","gtt", "fok", "fillOrKill"]}`) data = []byte(`{"tifs": ["abcd,POSTONLY,IOC", "GTC,POSTONLY", "GTC", "", "POSTONLY,IOC", "GoodTilCancel", "GoodTILLCANCEL", "POST_ONLY", "POC","IOC", "GTD", "gtd","gtt", "fok", "fillOrKill", "SOR"]}`)
target = &struct { target = &struct {
TIFs []TimeInForce `json:"tifs"` TIFs []TimeInForce `json:"tifs"`
}{} }{}

View File

@@ -5,6 +5,7 @@ import (
"sort" "sort"
"time" "time"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/common/key" "github.com/thrasher-corp/gocryptotrader/common/key"
"github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/dispatch" "github.com/thrasher-corp/gocryptotrader/dispatch"
@@ -80,7 +81,7 @@ func (s *store) track(b *Book) (book, error) {
// DeployDepth used for subsystem deployment creates a depth item in the struct then returns a ptr to that Depth item // DeployDepth used for subsystem deployment creates a depth item in the struct then returns a ptr to that Depth item
func (s *store) DeployDepth(exchange string, p currency.Pair, a asset.Item) (*Depth, error) { func (s *store) DeployDepth(exchange string, p currency.Pair, a asset.Item) (*Depth, error) {
if exchange == "" { if exchange == "" {
return nil, ErrExchangeNameEmpty return nil, common.ErrExchangeNameNotSet
} }
if p.IsEmpty() { if p.IsEmpty() {
return nil, errPairNotSet return nil, errPairNotSet
@@ -157,7 +158,7 @@ func (b *Book) TotalAsksAmount() (amountCollated, total float64) {
// list // list
func (b *Book) Process() error { func (b *Book) Process() error {
if b.Exchange == "" { if b.Exchange == "" {
return ErrExchangeNameEmpty return common.ErrExchangeNameNotSet
} }
if b.Pair.IsEmpty() { if b.Pair.IsEmpty() {

View File

@@ -12,6 +12,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/dispatch" "github.com/thrasher-corp/gocryptotrader/dispatch"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/asset"
@@ -223,7 +224,7 @@ func TestBookGetDepth(t *testing.T) {
func TestDeployDepth(t *testing.T) { func TestDeployDepth(t *testing.T) {
pair := currency.NewBTCUSD() pair := currency.NewBTCUSD()
_, err := DeployDepth("", pair, asset.Spot) _, err := DeployDepth("", pair, asset.Spot)
require.ErrorIs(t, err, ErrExchangeNameEmpty) require.ErrorIs(t, err, common.ErrExchangeNameNotSet)
_, err = DeployDepth("test", currency.EMPTYPAIR, asset.Spot) _, err = DeployDepth("test", currency.EMPTYPAIR, asset.Spot)
require.ErrorIs(t, err, errPairNotSet) require.ErrorIs(t, err, errPairNotSet)
_, err = DeployDepth("test", pair, asset.Empty) _, err = DeployDepth("test", pair, asset.Empty)

View File

@@ -102,7 +102,7 @@ func TestExpandTemplates(t *testing.T) {
equalLists(t, exp, got) equalLists(t, exp, got)
// Users can specify pairs which aren't available, even across diverse assets // Users can specify pairs which aren't available, even across diverse assets
// Use-case: Coinbasepro user sub for futures BTC-USD would return all BTC pairs and all USD pairs, even though BTC-USD might not be enabled or available // Use-case: Coinbase user sub for futures BTC-USD would return all BTC pairs and all USD pairs, even though BTC-USD might not be enabled or available
p := currency.Pairs{currency.NewPairWithDelimiter("BEAR", "PEAR", "🐻")} p := currency.Pairs{currency.NewPairWithDelimiter("BEAR", "PEAR", "🐻")}
got, err = List{{Channel: "expand-pairs", Asset: asset.All, Pairs: p}}.ExpandTemplates(e) got, err = List{{Channel: "expand-pairs", Asset: asset.All, Pairs: p}}.ExpandTemplates(e)
require.NoError(t, err, "Must not error with fictional pairs") require.NoError(t, err, "Must not error with fictional pairs")

View File

@@ -24,7 +24,7 @@ var Exchanges = []string{
"btc markets", "btc markets",
"btse", "btse",
"bybit", "bybit",
"coinbasepro", "coinbase",
"coinut", "coinut",
"deribit", "deribit",
"exmo", "exmo",

View File

@@ -7,6 +7,7 @@ import (
"time" "time"
"github.com/gofrs/uuid" "github.com/gofrs/uuid"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/common/key" "github.com/thrasher-corp/gocryptotrader/common/key"
"github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/dispatch" "github.com/thrasher-corp/gocryptotrader/dispatch"
@@ -15,9 +16,8 @@ import (
// Public errors // Public errors
var ( var (
ErrTickerNotFound = errors.New("no ticker found") ErrTickerNotFound = errors.New("no ticker found")
ErrBidEqualsAsk = errors.New("bid equals ask this is a crossed or locked market") ErrBidEqualsAsk = errors.New("bid equals ask this is a crossed or locked market")
ErrExchangeNameIsEmpty = errors.New("exchange name is empty")
) )
var ( var (
@@ -66,7 +66,7 @@ func SubscribeToExchangeTickers(exchange string) (dispatch.Pipe, error) {
// GetTicker checks and returns a requested ticker if it exists // GetTicker checks and returns a requested ticker if it exists
func GetTicker(exchange string, p currency.Pair, a asset.Item) (*Price, error) { func GetTicker(exchange string, p currency.Pair, a asset.Item) (*Price, error) {
if exchange == "" { if exchange == "" {
return nil, ErrExchangeNameIsEmpty return nil, common.ErrExchangeNameNotSet
} }
if p.IsEmpty() { if p.IsEmpty() {
return nil, currency.ErrCurrencyPairEmpty return nil, currency.ErrCurrencyPairEmpty
@@ -93,7 +93,7 @@ func GetExchangeTickers(exchange string) ([]*Price, error) {
func (s *Service) getExchangeTickers(exchange string) ([]*Price, error) { func (s *Service) getExchangeTickers(exchange string) ([]*Price, error) {
if exchange == "" { if exchange == "" {
return nil, ErrExchangeNameIsEmpty return nil, common.ErrExchangeNameNotSet
} }
exchange = strings.ToLower(exchange) exchange = strings.ToLower(exchange)
s.mu.Lock() s.mu.Lock()
@@ -136,7 +136,7 @@ func ProcessTicker(p *Price) error {
} }
if p.ExchangeName == "" { if p.ExchangeName == "" {
return ErrExchangeNameIsEmpty return common.ErrExchangeNameNotSet
} }
if p.Pair.IsEmpty() { if p.Pair.IsEmpty() {
@@ -223,7 +223,7 @@ func (s *Service) setItemID(t *Ticker, p *Price, exch string) error {
// getAssociations links a singular book with its dispatch associations // getAssociations links a singular book with its dispatch associations
func (s *Service) getAssociations(exch string) ([]uuid.UUID, error) { func (s *Service) getAssociations(exch string) ([]uuid.UUID, error) {
if exch == "" { if exch == "" {
return nil, ErrExchangeNameIsEmpty return nil, common.ErrExchangeNameNotSet
} }
var ids []uuid.UUID var ids []uuid.UUID
exchangeID, ok := s.Exchange[exch] exchangeID, ok := s.Exchange[exch]

View File

@@ -12,6 +12,7 @@ import (
"github.com/gofrs/uuid" "github.com/gofrs/uuid"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/common/key" "github.com/thrasher-corp/gocryptotrader/common/key"
"github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/dispatch" "github.com/thrasher-corp/gocryptotrader/dispatch"
@@ -423,7 +424,7 @@ func TestProcessTicker(t *testing.T) { // non-appending function to tickers
func TestGetAssociation(t *testing.T) { func TestGetAssociation(t *testing.T) {
_, err := service.getAssociations("") _, err := service.getAssociations("")
assert.ErrorIs(t, err, ErrExchangeNameIsEmpty) assert.ErrorIs(t, err, common.ErrExchangeNameNotSet)
service.mux = nil service.mux = nil
@@ -437,7 +438,7 @@ func TestGetAssociation(t *testing.T) {
func TestGetExchangeTickersPublic(t *testing.T) { func TestGetExchangeTickersPublic(t *testing.T) {
_, err := GetExchangeTickers("") _, err := GetExchangeTickers("")
assert.ErrorIs(t, err, ErrExchangeNameIsEmpty) assert.ErrorIs(t, err, common.ErrExchangeNameNotSet)
} }
func TestGetExchangeTickers(t *testing.T) { func TestGetExchangeTickers(t *testing.T) {
@@ -448,7 +449,7 @@ func TestGetExchangeTickers(t *testing.T) {
} }
_, err := s.getExchangeTickers("") _, err := s.getExchangeTickers("")
assert.ErrorIs(t, err, ErrExchangeNameIsEmpty) assert.ErrorIs(t, err, common.ErrExchangeNameNotSet)
_, err = s.getExchangeTickers("test") _, err = s.getExchangeTickers("test")
assert.ErrorIs(t, err, errExchangeNotFound) assert.ErrorIs(t, err, errExchangeNotFound)

View File

@@ -69,7 +69,7 @@ _b in this context is an `IBotExchange` implemented struct_
| BTCMarkets | Yes | Yes | No | | BTCMarkets | Yes | Yes | No |
| BTSE | Yes | Yes | No | | BTSE | Yes | Yes | No |
| Bybit | Yes | Yes | Yes | | Bybit | Yes | Yes | Yes |
| CoinbasePro | Yes | Yes | No| | Coinbase | Yes | Yes | No|
| COINUT | Yes | Yes | No | | COINUT | Yes | Yes | No |
| Deribit | Yes | Yes | Yes | | Deribit | Yes | Yes | Yes |
| Exmo | Yes | NA | No | | Exmo | Yes | NA | No |

View File

@@ -6,7 +6,7 @@ load := func() {
start := t.add(t.now(), -t.hour*24) start := t.add(t.now(), -t.hour*24)
// 'ctx' is already defined when we construct our bytecode from file. // 'ctx' is already defined when we construct our bytecode from file.
// To add debugging information to the request, see verbose.gct. To add account credentials, see account.gct // To add debugging information to the request, see verbose.gct. To add account credentials, see account.gct
ohlcvData := exch.ohlcv(ctx, "coinbasepro", "BTC-USD", "-", "SPOT", start, t.now(), "1h") ohlcvData := exch.ohlcv(ctx, "coinbase", "BTC-USD", "-", "SPOT", start, t.now(), "1h")
if is_error(ohlcvData) { if is_error(ohlcvData) {
// handle error // handle error
} }

View File

@@ -4,6 +4,7 @@ import (
"errors" "errors"
"strings" "strings"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/validate" "github.com/thrasher-corp/gocryptotrader/exchanges/validate"
) )
@@ -15,7 +16,7 @@ func (r *Request) Validate(opt ...validate.Checker) (err error) {
} }
if r.Exchange == "" { if r.Exchange == "" {
return ErrExchangeNameUnset return common.ErrExchangeNameNotSet
} }
var allErrors []string var allErrors []string

View File

@@ -6,6 +6,7 @@ import (
"os" "os"
"testing" "testing"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/core" "github.com/thrasher-corp/gocryptotrader/core"
"github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/validate" "github.com/thrasher-corp/gocryptotrader/exchanges/validate"
@@ -151,7 +152,7 @@ func TestExchangeNameUnset(t *testing.T) {
r := Request{} r := Request{}
err := r.Validate() err := r.Validate()
if err != nil { if err != nil {
if err != ErrExchangeNameUnset { if err != common.ErrExchangeNameNotSet {
t.Fatal(err) t.Fatal(err)
} }
} }

View File

@@ -40,8 +40,6 @@ const (
var ( var (
// ErrRequestCannotBeNil message to return when a request is nil // ErrRequestCannotBeNil message to return when a request is nil
ErrRequestCannotBeNil = errors.New("request cannot be nil") ErrRequestCannotBeNil = errors.New("request cannot be nil")
// ErrExchangeNameUnset message to return when an exchange name is unset
ErrExchangeNameUnset = errors.New("exchange name unset")
// ErrInvalidRequest message to return when a request type is invalid // ErrInvalidRequest message to return when a request type is invalid
ErrInvalidRequest = errors.New("invalid request type") ErrInvalidRequest = errors.New("invalid request type")
// ErrStrAddressNotWhiteListed occurs when a withdrawal attempts to withdraw from a non-whitelisted address // ErrStrAddressNotWhiteListed occurs when a withdrawal attempts to withdraw from a non-whitelisted address
@@ -83,6 +81,27 @@ type FiatRequest struct {
WireCurrency string WireCurrency string
} }
// TravelAddress holds the address information required for travel rule compliance
type TravelAddress struct {
Address1 string
Address2 string
Address3 string
City string
State string
Country string
PostalCode string
}
// TravelRule stores the information that may need to be provided to comply with local regulations
type TravelRule struct {
BeneficiaryWalletType string
IsSelf bool
BeneficiaryName string
BeneficiaryAddress TravelAddress
BeneficiaryFinancialInstitution string
TransferPurpose string
}
// Request holds complete details for request // Request holds complete details for request
type Request struct { type Request struct {
Exchange string `json:"exchange"` Exchange string `json:"exchange"`
@@ -91,9 +110,10 @@ type Request struct {
Amount float64 `json:"amount"` Amount float64 `json:"amount"`
Type RequestType `json:"type"` Type RequestType `json:"type"`
// Used exclusively in Binance.US
ClientOrderID string `json:"clientID"` ClientOrderID string `json:"clientID"`
WalletID string `json:"walletID"`
// Used exclusively in OKX to classify internal represented by '3' or on chain represented by '4' // Used exclusively in OKX to classify internal represented by '3' or on chain represented by '4'
InternalTransfer bool InternalTransfer bool
@@ -103,6 +123,10 @@ type Request struct {
Crypto CryptoRequest `json:"crypto"` Crypto CryptoRequest `json:"crypto"`
Fiat FiatRequest `json:"fiat"` Fiat FiatRequest `json:"fiat"`
Travel TravelRule `json:"travel_rule"`
IdempotencyToken string
} }
// Response holds complete details for Response // Response holds complete details for Response

View File

@@ -1266,7 +1266,7 @@
] ]
}, },
{ {
"name": "CoinbasePro", "name": "Coinbase",
"enabled": true, "enabled": true,
"verbose": false, "verbose": false,
"httpTimeout": 15000000000, "httpTimeout": 15000000000,
@@ -1286,12 +1286,17 @@
}, },
"useGlobalFormat": true, "useGlobalFormat": true,
"assetTypes": [ "assetTypes": [
"spot" "spot",
"futures"
], ],
"pairs": { "pairs": {
"spot": { "spot": {
"enabled": "BTC-USD", "enabled": "BTC-USD",
"available": "LTC-GBP,XLM-BTC,DASH-BTC,DAI-USDC,ZEC-USDC,XLM-EUR,ZRX-BTC,LTC-BTC,ETC-BTC,ETH-USD,XRP-EUR,BTC-USDC,REP-USD,EOS-BTC,ZEC-BTC,ETC-GBP,LINK-ETH,XRP-BTC,ZRX-USD,ETH-USDC,MANA-USDC,BTC-EUR,BCH-GBP,DNT-USDC,EOS-EUR,BCH-EUR,LTC-EUR,CVC-USDC,ETH-GBP,DASH-USD,ETH-EUR,XTZ-BTC,ZRX-EUR,BAT-ETH,BTC-GBP,ETC-USD,BAT-USDC,BCH-USD,GNT-USDC,ALGO-USD,LINK-USD,XLM-USD,ETH-BTC,EOS-USD,REP-BTC,ETH-DAI,XRP-USD,LTC-USD,ETC-EUR,BTC-USD,XTZ-USD,BCH-BTC,LOOM-USDC" "available": "LTC-GBP,XLM-BTC,DASH-BTC,DAI-USDC,ZEC-USDC,XLM-EUR,ZRX-BTC,LTC-BTC,ETC-BTC,ETH-USD,XRP-EUR,BTC-USDC,REP-USD,EOS-BTC,ZEC-BTC,ETC-GBP,LINK-ETH,XRP-BTC,ZRX-USD,ETH-USDC,MANA-USDC,BTC-EUR,BCH-GBP,DNT-USDC,EOS-EUR,BCH-EUR,LTC-EUR,CVC-USDC,ETH-GBP,DASH-USD,ETH-EUR,XTZ-BTC,ZRX-EUR,BAT-ETH,BTC-GBP,ETC-USD,BAT-USDC,BCH-USD,GNT-USDC,ALGO-USD,LINK-USD,XLM-USD,ETH-BTC,EOS-USD,REP-BTC,ETH-DAI,XRP-USD,LTC-USD,ETC-EUR,BTC-USD,XTZ-USD,BCH-BTC,LOOM-USDC"
},
"futures": {
"enabled": "BTC-USD",
"available": "BTC-USD,BTC-PERP-INTX"
} }
} }
}, },

View File

@@ -8,7 +8,7 @@ bitstamp,
btc markets, btc markets,
btse, btse,
bybit, bybit,
coinbasepro, coinbase,
coinut, coinut,
deribit, deribit,
exmo, exmo,
1 binanceus
8 btc markets
9 btse
10 bybit
11 coinbasepro coinbase
12 coinut
13 deribit
14 exmo