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