mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-14 07:26:47 +00:00
* exchanges: add setTimeWindow boolean to GetKlineRequest params to differentiate between a set time period return from endpoint. * glorious: nits * exchange: conjugation * Update exchanges/exchange.go Co-authored-by: Scott <gloriousCode@users.noreply.github.com> * glorious: nits and an assortment of differences * exchanges: remove some comments * glorious: nits * cleanup * tests: fix * Update exchanges/hitbtc/hitbtc_wrapper.go Co-authored-by: Scott <gloriousCode@users.noreply.github.com> * Update exchanges/kline/kline.go Co-authored-by: Scott <gloriousCode@users.noreply.github.com> * Update exchanges/kline/kline_test.go Co-authored-by: Scott <gloriousCode@users.noreply.github.com> * glorious: nits * kline: fix test * rm unused variables * almost: nits * glorious: nits * linter: fix * rm unused variable * Refactored comment in the okex tests to ensure that it accurately reflects the variable name and the issue related to the time window, as requested by GloriousCode. The previous comment did not align with the identifier assigned to the property, which could cause confusion and misunderstanding among other programmers or stakeholders. The updated comment will improve the clarity and readability of the codebase and make it easier to understand the intended purpose of the associated variables. The change was made with the aim of improving the overall quality and maintainability of the code. --------- Co-authored-by: Ryan O'Hara-Reid <ryan.oharareid@thrasher.io> Co-authored-by: Scott <gloriousCode@users.noreply.github.com>
1630 lines
50 KiB
Go
1630 lines
50 KiB
Go
package exchange
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/thrasher-corp/gocryptotrader/common"
|
|
"github.com/thrasher-corp/gocryptotrader/common/convert"
|
|
"github.com/thrasher-corp/gocryptotrader/config"
|
|
"github.com/thrasher-corp/gocryptotrader/currency"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/account"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/currencystate"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/margin"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/protocol"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/stream"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/trade"
|
|
"github.com/thrasher-corp/gocryptotrader/log"
|
|
"github.com/thrasher-corp/gocryptotrader/portfolio/banking"
|
|
)
|
|
|
|
const (
|
|
warningBase64DecryptSecretKeyFailed = "exchange %s unable to base64 decode secret key.. Disabling Authenticated API support" //nolint // False positive (G101: Potential hardcoded credentials)
|
|
// DefaultHTTPTimeout is the default HTTP/HTTPS Timeout for exchange requests
|
|
DefaultHTTPTimeout = time.Second * 15
|
|
// DefaultWebsocketResponseCheckTimeout is the default delay in checking for an expected websocket response
|
|
DefaultWebsocketResponseCheckTimeout = time.Millisecond * 50
|
|
// DefaultWebsocketResponseMaxLimit is the default max wait for an expected websocket response before a timeout
|
|
DefaultWebsocketResponseMaxLimit = time.Second * 7
|
|
// DefaultWebsocketOrderbookBufferLimit is the maximum number of orderbook updates that get stored before being applied
|
|
DefaultWebsocketOrderbookBufferLimit = 5
|
|
// ResetConfigPairsWarningMessage is displayed when a currency pair format in the config needs to be updated
|
|
ResetConfigPairsWarningMessage = "%s Enabled and available pairs for %s reset due to config upgrade, please enable the ones you would like to use again. Defaulting to %v"
|
|
)
|
|
|
|
var (
|
|
errEndpointStringNotFound = errors.New("endpoint string not found")
|
|
errConfigPairFormatRequiresDelimiter = errors.New("config pair format requires delimiter")
|
|
errSymbolCannotBeMatched = errors.New("symbol cannot be matched")
|
|
errGlobalRequestFormatIsNil = errors.New("global request format is nil")
|
|
errGlobalConfigFormatIsNil = errors.New("global config format is nil")
|
|
errAssetRequestFormatIsNil = errors.New("asset type request format is nil")
|
|
errAssetConfigFormatIsNil = errors.New("asset type config format is nil")
|
|
)
|
|
|
|
// SetRequester sets the instance of the requester
|
|
func (b *Base) SetRequester(r *request.Requester) error {
|
|
if r == nil {
|
|
return fmt.Errorf("%s cannot set requester, no requester provided", b.Name)
|
|
}
|
|
|
|
b.Requester = r
|
|
return nil
|
|
}
|
|
|
|
// SetClientProxyAddress sets a proxy address for REST and websocket requests
|
|
func (b *Base) SetClientProxyAddress(addr string) error {
|
|
if addr == "" {
|
|
return nil
|
|
}
|
|
proxy, err := url.Parse(addr)
|
|
if err != nil {
|
|
return fmt.Errorf("setting proxy address error %s",
|
|
err)
|
|
}
|
|
|
|
err = b.Requester.SetProxy(proxy)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if b.Websocket != nil {
|
|
err = b.Websocket.SetProxyAddress(addr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// SetFeatureDefaults sets the exchanges default feature support set
|
|
func (b *Base) SetFeatureDefaults() {
|
|
if b.Config.Features == nil {
|
|
s := &config.FeaturesConfig{
|
|
Supports: config.FeaturesSupportedConfig{
|
|
Websocket: b.Features.Supports.Websocket,
|
|
REST: b.Features.Supports.REST,
|
|
RESTCapabilities: protocol.Features{
|
|
AutoPairUpdates: b.Features.Supports.RESTCapabilities.AutoPairUpdates,
|
|
},
|
|
},
|
|
}
|
|
|
|
if b.Config.SupportsAutoPairUpdates != nil {
|
|
s.Supports.RESTCapabilities.AutoPairUpdates = *b.Config.SupportsAutoPairUpdates
|
|
s.Enabled.AutoPairUpdates = *b.Config.SupportsAutoPairUpdates
|
|
} else {
|
|
s.Supports.RESTCapabilities.AutoPairUpdates = b.Features.Supports.RESTCapabilities.AutoPairUpdates
|
|
s.Enabled.AutoPairUpdates = b.Features.Supports.RESTCapabilities.AutoPairUpdates
|
|
if !s.Supports.RESTCapabilities.AutoPairUpdates {
|
|
b.Config.CurrencyPairs.LastUpdated = time.Now().Unix()
|
|
b.CurrencyPairs.LastUpdated = b.Config.CurrencyPairs.LastUpdated
|
|
}
|
|
}
|
|
b.Config.Features = s
|
|
b.Config.SupportsAutoPairUpdates = nil
|
|
} else {
|
|
if b.Features.Supports.RESTCapabilities.AutoPairUpdates != b.Config.Features.Supports.RESTCapabilities.AutoPairUpdates {
|
|
b.Config.Features.Supports.RESTCapabilities.AutoPairUpdates = b.Features.Supports.RESTCapabilities.AutoPairUpdates
|
|
|
|
if !b.Config.Features.Supports.RESTCapabilities.AutoPairUpdates {
|
|
b.Config.CurrencyPairs.LastUpdated = time.Now().Unix()
|
|
}
|
|
}
|
|
|
|
if b.Features.Supports.REST != b.Config.Features.Supports.REST {
|
|
b.Config.Features.Supports.REST = b.Features.Supports.REST
|
|
}
|
|
|
|
if b.Features.Supports.RESTCapabilities.TickerBatching != b.Config.Features.Supports.RESTCapabilities.TickerBatching {
|
|
b.Config.Features.Supports.RESTCapabilities.TickerBatching = b.Features.Supports.RESTCapabilities.TickerBatching
|
|
}
|
|
|
|
if b.Features.Supports.Websocket != b.Config.Features.Supports.Websocket {
|
|
b.Config.Features.Supports.Websocket = b.Features.Supports.Websocket
|
|
}
|
|
|
|
if b.IsSaveTradeDataEnabled() != b.Config.Features.Enabled.SaveTradeData {
|
|
b.SetSaveTradeDataStatus(b.Config.Features.Enabled.SaveTradeData)
|
|
}
|
|
|
|
if b.IsTradeFeedEnabled() != b.Config.Features.Enabled.TradeFeed {
|
|
b.SetTradeFeedStatus(b.Config.Features.Enabled.TradeFeed)
|
|
}
|
|
|
|
if b.IsFillsFeedEnabled() != b.Config.Features.Enabled.FillsFeed {
|
|
b.SetFillsFeedStatus(b.Config.Features.Enabled.FillsFeed)
|
|
}
|
|
|
|
b.Features.Enabled.AutoPairUpdates = b.Config.Features.Enabled.AutoPairUpdates
|
|
}
|
|
}
|
|
|
|
// SupportsRESTTickerBatchUpdates returns whether or not the
|
|
// exchange supports REST batch ticker fetching
|
|
func (b *Base) SupportsRESTTickerBatchUpdates() bool {
|
|
return b.Features.Supports.RESTCapabilities.TickerBatching
|
|
}
|
|
|
|
// SupportsAutoPairUpdates returns whether or not the exchange supports
|
|
// auto currency pair updating
|
|
func (b *Base) SupportsAutoPairUpdates() bool {
|
|
return b.Features.Supports.RESTCapabilities.AutoPairUpdates ||
|
|
b.Features.Supports.WebsocketCapabilities.AutoPairUpdates
|
|
}
|
|
|
|
// GetLastPairsUpdateTime returns the unix timestamp of when the exchanges
|
|
// currency pairs were last updated
|
|
func (b *Base) GetLastPairsUpdateTime() int64 {
|
|
return b.CurrencyPairs.LastUpdated
|
|
}
|
|
|
|
// GetAssetTypes returns the either the enabled or available asset types for an
|
|
// individual exchange
|
|
func (b *Base) GetAssetTypes(enabled bool) asset.Items {
|
|
return b.CurrencyPairs.GetAssetTypes(enabled)
|
|
}
|
|
|
|
// GetPairAssetType returns the associated asset type for the currency pair
|
|
// This method is only useful for exchanges that have pair names with multiple delimiters (BTC-USD-0626)
|
|
// Helpful if the exchange has only a single asset type but in that case the asset type can be hard coded
|
|
func (b *Base) GetPairAssetType(c currency.Pair) (asset.Item, error) {
|
|
assetTypes := b.GetAssetTypes(false)
|
|
for i := range assetTypes {
|
|
avail, err := b.GetAvailablePairs(assetTypes[i])
|
|
if err != nil {
|
|
return asset.Empty, err
|
|
}
|
|
if avail.Contains(c, true) {
|
|
return assetTypes[i], nil
|
|
}
|
|
}
|
|
return asset.Empty, errors.New("asset type not associated with currency pair")
|
|
}
|
|
|
|
// GetPairAndAssetTypeRequestFormatted returns the pair and the asset type
|
|
// when there is distinct differentiation between exchange request symbols asset
|
|
// types. e.g. "BTC-USD" Spot and "BTC_USD" PERP request formatted.
|
|
func (b *Base) GetPairAndAssetTypeRequestFormatted(symbol string) (currency.Pair, asset.Item, error) {
|
|
if symbol == "" {
|
|
return currency.Pair{}, asset.Empty, currency.ErrCurrencyPairEmpty
|
|
}
|
|
assetTypes := b.GetAssetTypes(true)
|
|
for i := range assetTypes {
|
|
pFmt, err := b.GetPairFormat(assetTypes[i], true)
|
|
if err != nil {
|
|
return currency.Pair{}, asset.Empty, err
|
|
}
|
|
|
|
enabled, err := b.GetEnabledPairs(assetTypes[i])
|
|
if err != nil {
|
|
return currency.Pair{}, asset.Empty, err
|
|
}
|
|
for j := range enabled {
|
|
if pFmt.Format(enabled[j]) == symbol {
|
|
return enabled[j], assetTypes[i], nil
|
|
}
|
|
}
|
|
}
|
|
return currency.Pair{}, asset.Empty, errSymbolCannotBeMatched
|
|
}
|
|
|
|
// GetClientBankAccounts returns banking details associated with
|
|
// a client for withdrawal purposes
|
|
func (b *Base) GetClientBankAccounts(exchangeName, withdrawalCurrency string) (*banking.Account, error) {
|
|
cfg := config.GetConfig()
|
|
return cfg.GetClientBankAccounts(exchangeName, withdrawalCurrency)
|
|
}
|
|
|
|
// GetExchangeBankAccounts returns banking details associated with an
|
|
// exchange for funding purposes
|
|
func (b *Base) GetExchangeBankAccounts(id, depositCurrency string) (*banking.Account, error) {
|
|
cfg := config.GetConfig()
|
|
return cfg.GetExchangeBankAccounts(b.Name, id, depositCurrency)
|
|
}
|
|
|
|
// SetCurrencyPairFormat checks the exchange request and config currency pair
|
|
// formats and syncs it with the exchanges SetDefault settings
|
|
func (b *Base) SetCurrencyPairFormat() error {
|
|
if b.Config.CurrencyPairs == nil {
|
|
b.Config.CurrencyPairs = new(currency.PairsManager)
|
|
}
|
|
|
|
b.Config.CurrencyPairs.UseGlobalFormat = b.CurrencyPairs.UseGlobalFormat
|
|
if b.Config.CurrencyPairs.UseGlobalFormat {
|
|
b.Config.CurrencyPairs.RequestFormat = b.CurrencyPairs.RequestFormat
|
|
b.Config.CurrencyPairs.ConfigFormat = b.CurrencyPairs.ConfigFormat
|
|
return nil
|
|
}
|
|
|
|
if b.Config.CurrencyPairs.ConfigFormat != nil {
|
|
b.Config.CurrencyPairs.ConfigFormat = nil
|
|
}
|
|
if b.Config.CurrencyPairs.RequestFormat != nil {
|
|
b.Config.CurrencyPairs.RequestFormat = nil
|
|
}
|
|
|
|
assetTypes := b.GetAssetTypes(false)
|
|
for x := range assetTypes {
|
|
if _, err := b.Config.CurrencyPairs.Get(assetTypes[x]); err != nil {
|
|
ps, err := b.CurrencyPairs.Get(assetTypes[x])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = b.Config.CurrencyPairs.Store(assetTypes[x], ps)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// SetConfigPairs sets the exchanges currency pairs to the pairs set in the config
|
|
func (b *Base) SetConfigPairs() error {
|
|
assetTypes := b.Config.CurrencyPairs.GetAssetTypes(false)
|
|
exchangeAssets := b.CurrencyPairs.GetAssetTypes(false)
|
|
for x := range assetTypes {
|
|
if !exchangeAssets.Contains(assetTypes[x]) {
|
|
log.Warnf(log.ExchangeSys,
|
|
"%s exchange asset type %s unsupported, please manually remove from configuration",
|
|
b.Name,
|
|
assetTypes[x])
|
|
continue // If there are unsupported assets contained in config, skip.
|
|
}
|
|
|
|
var enabledAsset bool
|
|
if b.Config.CurrencyPairs.IsAssetEnabled(assetTypes[x]) == nil {
|
|
enabledAsset = true
|
|
}
|
|
|
|
err := b.CurrencyPairs.SetAssetEnabled(assetTypes[x], enabledAsset)
|
|
// Suppress error when assets are enabled by default and they are being
|
|
// enabled by config. A check for the inverse
|
|
// e.g. currency.ErrAssetAlreadyDisabled is not needed.
|
|
if err != nil && !errors.Is(err, currency.ErrAssetAlreadyEnabled) {
|
|
return err
|
|
}
|
|
|
|
cfgPS, err := b.Config.CurrencyPairs.Get(assetTypes[x])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if b.Config.CurrencyPairs.UseGlobalFormat {
|
|
err = b.CurrencyPairs.StorePairs(assetTypes[x], cfgPS.Available, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = b.CurrencyPairs.StorePairs(assetTypes[x], cfgPS.Enabled, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
continue
|
|
}
|
|
exchPS, err := b.CurrencyPairs.Get(assetTypes[x])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if exchPS.ConfigFormat != nil {
|
|
err = b.Config.CurrencyPairs.StoreFormat(assetTypes[x], exchPS.ConfigFormat, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if exchPS.RequestFormat != nil {
|
|
err = b.Config.CurrencyPairs.StoreFormat(assetTypes[x], exchPS.RequestFormat, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
err = b.CurrencyPairs.StorePairs(assetTypes[x], cfgPS.Available, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = b.CurrencyPairs.StorePairs(assetTypes[x], cfgPS.Enabled, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetName is a method that returns the name of the exchange base
|
|
func (b *Base) GetName() string {
|
|
return b.Name
|
|
}
|
|
|
|
// GetEnabledFeatures returns the exchanges enabled features
|
|
func (b *Base) GetEnabledFeatures() FeaturesEnabled {
|
|
return b.Features.Enabled
|
|
}
|
|
|
|
// GetSupportedFeatures returns the exchanges supported features
|
|
func (b *Base) GetSupportedFeatures() FeaturesSupported {
|
|
return b.Features.Supports
|
|
}
|
|
|
|
// GetPairFormat returns the pair format based on the exchange and
|
|
// asset type
|
|
func (b *Base) GetPairFormat(assetType asset.Item, requestFormat bool) (currency.PairFormat, error) {
|
|
if b.CurrencyPairs.UseGlobalFormat {
|
|
if requestFormat {
|
|
if b.CurrencyPairs.RequestFormat == nil {
|
|
return currency.EMPTYFORMAT, errGlobalRequestFormatIsNil
|
|
}
|
|
return *b.CurrencyPairs.RequestFormat, nil
|
|
}
|
|
|
|
if b.CurrencyPairs.ConfigFormat == nil {
|
|
return currency.EMPTYFORMAT, errGlobalConfigFormatIsNil
|
|
}
|
|
return *b.CurrencyPairs.ConfigFormat, nil
|
|
}
|
|
|
|
ps, err := b.CurrencyPairs.Get(assetType)
|
|
if err != nil {
|
|
return currency.EMPTYFORMAT, err
|
|
}
|
|
|
|
if requestFormat {
|
|
if ps.RequestFormat == nil {
|
|
return currency.EMPTYFORMAT, errAssetRequestFormatIsNil
|
|
}
|
|
return *ps.RequestFormat, nil
|
|
}
|
|
|
|
if ps.ConfigFormat == nil {
|
|
return currency.EMPTYFORMAT, errAssetConfigFormatIsNil
|
|
}
|
|
return *ps.ConfigFormat, nil
|
|
}
|
|
|
|
// GetEnabledPairs is a method that returns the enabled currency pairs of
|
|
// the exchange by asset type, if the asset type is disabled this will return no
|
|
// enabled pairs
|
|
func (b *Base) GetEnabledPairs(a asset.Item) (currency.Pairs, error) {
|
|
err := b.CurrencyPairs.IsAssetEnabled(a)
|
|
if err != nil {
|
|
return nil, nil //nolint:nilerr // non-fatal error
|
|
}
|
|
format, err := b.GetPairFormat(a, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
enabledPairs, err := b.CurrencyPairs.GetPairs(a, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return enabledPairs.Format(format), nil
|
|
}
|
|
|
|
// GetRequestFormattedPairAndAssetType is a method that returns the enabled currency pair of
|
|
// along with its asset type. Only use when there is no chance of the same name crossing over
|
|
func (b *Base) GetRequestFormattedPairAndAssetType(p string) (currency.Pair, asset.Item, error) {
|
|
assetTypes := b.GetAssetTypes(true)
|
|
for i := range assetTypes {
|
|
format, err := b.GetPairFormat(assetTypes[i], true)
|
|
if err != nil {
|
|
return currency.EMPTYPAIR, assetTypes[i], err
|
|
}
|
|
|
|
pairs, err := b.CurrencyPairs.GetPairs(assetTypes[i], true)
|
|
if err != nil {
|
|
return currency.EMPTYPAIR, assetTypes[i], err
|
|
}
|
|
|
|
for j := range pairs {
|
|
formattedPair := pairs[j].Format(format)
|
|
if strings.EqualFold(formattedPair.String(), p) {
|
|
return formattedPair, assetTypes[i], nil
|
|
}
|
|
}
|
|
}
|
|
return currency.EMPTYPAIR, asset.Empty, fmt.Errorf("%s %w", p, currency.ErrPairNotFound)
|
|
}
|
|
|
|
// GetAvailablePairs is a method that returns the available currency pairs
|
|
// of the exchange by asset type
|
|
func (b *Base) GetAvailablePairs(assetType asset.Item) (currency.Pairs, error) {
|
|
format, err := b.GetPairFormat(assetType, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pairs, err := b.CurrencyPairs.GetPairs(assetType, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return pairs.Format(format), nil
|
|
}
|
|
|
|
// SupportsPair returns true or not whether a currency pair exists in the
|
|
// exchange available currencies or not
|
|
func (b *Base) SupportsPair(p currency.Pair, enabledPairs bool, assetType asset.Item) error {
|
|
var pairs currency.Pairs
|
|
var err error
|
|
if enabledPairs {
|
|
pairs, err = b.GetEnabledPairs(assetType)
|
|
} else {
|
|
pairs, err = b.GetAvailablePairs(assetType)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if pairs.Contains(p, false) {
|
|
return nil
|
|
}
|
|
return errors.New("pair not supported")
|
|
}
|
|
|
|
// FormatExchangeCurrencies returns a string containing
|
|
// the exchanges formatted currency pairs
|
|
func (b *Base) FormatExchangeCurrencies(pairs []currency.Pair, assetType asset.Item) (string, error) {
|
|
var currencyItems strings.Builder
|
|
pairFmt, err := b.GetPairFormat(assetType, true)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
for x := range pairs {
|
|
format, err := b.FormatExchangeCurrency(pairs[x], assetType)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
currencyItems.WriteString(format.String())
|
|
if x == len(pairs)-1 {
|
|
continue
|
|
}
|
|
currencyItems.WriteString(pairFmt.Separator)
|
|
}
|
|
|
|
if currencyItems.Len() == 0 {
|
|
return "", errors.New("returned empty string")
|
|
}
|
|
return currencyItems.String(), nil
|
|
}
|
|
|
|
// FormatExchangeCurrency is a method that formats and returns a currency pair
|
|
// based on the user currency display preferences
|
|
func (b *Base) FormatExchangeCurrency(p currency.Pair, assetType asset.Item) (currency.Pair, error) {
|
|
pairFmt, err := b.GetPairFormat(assetType, true)
|
|
if err != nil {
|
|
return currency.EMPTYPAIR, err
|
|
}
|
|
return p.Format(pairFmt), nil
|
|
}
|
|
|
|
// SetEnabled is a method that sets if the exchange is enabled
|
|
func (b *Base) SetEnabled(enabled bool) {
|
|
b.Enabled = enabled
|
|
}
|
|
|
|
// IsEnabled is a method that returns if the current exchange is enabled
|
|
func (b *Base) IsEnabled() bool {
|
|
return b != nil && b.Enabled
|
|
}
|
|
|
|
// SetupDefaults sets the exchange settings based on the supplied config
|
|
func (b *Base) SetupDefaults(exch *config.Exchange) error {
|
|
err := exch.Validate()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
b.Enabled = true
|
|
b.LoadedByConfig = true
|
|
b.Config = exch
|
|
b.Verbose = exch.Verbose
|
|
|
|
b.API.AuthenticatedSupport = exch.API.AuthenticatedSupport
|
|
b.API.AuthenticatedWebsocketSupport = exch.API.AuthenticatedWebsocketSupport
|
|
if b.API.credentials == nil {
|
|
b.API.credentials = &account.Credentials{}
|
|
}
|
|
b.API.credentials.SubAccount = exch.API.Credentials.Subaccount
|
|
if b.API.AuthenticatedSupport || b.API.AuthenticatedWebsocketSupport {
|
|
b.SetCredentials(exch.API.Credentials.Key,
|
|
exch.API.Credentials.Secret,
|
|
exch.API.Credentials.ClientID,
|
|
exch.API.Credentials.Subaccount,
|
|
exch.API.Credentials.PEMKey,
|
|
exch.API.Credentials.OTPSecret,
|
|
)
|
|
}
|
|
|
|
if exch.HTTPTimeout <= time.Duration(0) {
|
|
exch.HTTPTimeout = DefaultHTTPTimeout
|
|
}
|
|
|
|
err = b.SetHTTPClientTimeout(exch.HTTPTimeout)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if exch.CurrencyPairs == nil {
|
|
exch.CurrencyPairs = new(currency.PairsManager)
|
|
}
|
|
|
|
b.HTTPDebugging = exch.HTTPDebugging
|
|
b.BypassConfigFormatUpgrades = exch.CurrencyPairs.BypassConfigFormatUpgrades
|
|
err = b.SetHTTPClientUserAgent(exch.HTTPUserAgent)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = b.SetCurrencyPairFormat()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = b.SetConfigPairs()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
b.SetFeatureDefaults()
|
|
|
|
if b.API.Endpoints == nil {
|
|
b.API.Endpoints = b.NewEndpoints()
|
|
}
|
|
|
|
err = b.SetAPIURL()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
b.SetAPICredentialDefaults()
|
|
|
|
err = b.SetClientProxyAddress(exch.ProxyAddress)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
b.BaseCurrencies = exch.BaseCurrencies
|
|
|
|
if exch.Orderbook.VerificationBypass {
|
|
log.Warnf(log.ExchangeSys,
|
|
"%s orderbook verification has been bypassed via config.",
|
|
b.Name)
|
|
}
|
|
b.CanVerifyOrderbook = !exch.Orderbook.VerificationBypass
|
|
b.States = currencystate.NewCurrencyStates()
|
|
return err
|
|
}
|
|
|
|
// SetPairs sets the exchange currency pairs for either enabledPairs or
|
|
// availablePairs
|
|
func (b *Base) SetPairs(pairs currency.Pairs, assetType asset.Item, enabled bool) error {
|
|
if len(pairs) == 0 {
|
|
return fmt.Errorf("%s SetPairs error - pairs is empty", b.Name)
|
|
}
|
|
|
|
pairFmt, err := b.GetPairFormat(assetType, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for x := range pairs {
|
|
pairs[x] = pairs[x].Format(pairFmt)
|
|
}
|
|
|
|
err = b.CurrencyPairs.StorePairs(assetType, pairs, enabled)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return b.Config.CurrencyPairs.StorePairs(assetType, pairs, enabled)
|
|
}
|
|
|
|
// UpdatePairs updates the exchange currency pairs for either enabledPairs or
|
|
// availablePairs
|
|
func (b *Base) UpdatePairs(incoming currency.Pairs, a asset.Item, enabled, force bool) error {
|
|
pFmt, err := b.GetPairFormat(a, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
incoming, err = incoming.ValidateAndConform(pFmt, b.BypassConfigFormatUpgrades)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
oldPairs, err := b.CurrencyPairs.GetPairs(a, enabled)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
diff, err := oldPairs.FindDifferences(incoming, pFmt)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if force || len(diff.New) != 0 || len(diff.Remove) != 0 || diff.FormatDifference {
|
|
var updateType string
|
|
if enabled {
|
|
updateType = "enabled"
|
|
} else {
|
|
updateType = "available"
|
|
}
|
|
|
|
if force {
|
|
log.Debugf(log.ExchangeSys,
|
|
"%s forced update of %s [%v] pairs.",
|
|
b.Name,
|
|
updateType,
|
|
strings.ToUpper(a.String()))
|
|
} else {
|
|
if len(diff.New) > 0 {
|
|
log.Debugf(log.ExchangeSys,
|
|
"%s Updating %s pairs [%v] - Added: %s.\n",
|
|
b.Name,
|
|
updateType,
|
|
strings.ToUpper(a.String()),
|
|
diff.New)
|
|
}
|
|
if len(diff.Remove) > 0 {
|
|
log.Debugf(log.ExchangeSys,
|
|
"%s Updating %s pairs [%v] - Removed: %s.\n",
|
|
b.Name,
|
|
updateType,
|
|
strings.ToUpper(a.String()),
|
|
diff.Remove)
|
|
}
|
|
}
|
|
err = b.Config.CurrencyPairs.StorePairs(a, incoming, enabled)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = b.CurrencyPairs.StorePairs(a, incoming, enabled)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if enabled {
|
|
return nil
|
|
}
|
|
|
|
// This section checks for differences after an available pairs adjustment
|
|
// which will remove currency pairs from enabled pairs that have been
|
|
// disabled by an exchange, adjust the entire list of enabled pairs if there
|
|
// is a required formatting change and it will also capture unintentional
|
|
// client inputs e.g. a client can enter `linkusd` via config and loaded
|
|
// into memory that might be unintentionally formatted too `lin-kusd` it
|
|
// will match that against the correct available pair in memory and apply
|
|
// correct formatting (LINK-USD) instead of being removed altogether which
|
|
// will require a shutdown and update of the config file to enable that
|
|
// asset.
|
|
|
|
enabledPairs, err := b.CurrencyPairs.GetPairs(a, true)
|
|
if err != nil &&
|
|
!errors.Is(err, currency.ErrPairNotContainedInAvailablePairs) &&
|
|
!errors.Is(err, currency.ErrPairDuplication) {
|
|
return err
|
|
}
|
|
|
|
if err == nil && !enabledPairs.HasFormatDifference(pFmt) {
|
|
return nil
|
|
}
|
|
|
|
diff, err = enabledPairs.FindDifferences(incoming, pFmt)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
check := make(map[string]bool)
|
|
var target int
|
|
for x := range enabledPairs {
|
|
pairNoFmt := currency.EMPTYFORMAT.Format(enabledPairs[x])
|
|
if check[pairNoFmt] {
|
|
diff.Remove = diff.Remove.Add(enabledPairs[x])
|
|
continue
|
|
}
|
|
check[pairNoFmt] = true
|
|
|
|
if !diff.Remove.Contains(enabledPairs[x], true) {
|
|
enabledPairs[target] = enabledPairs[x].Format(pFmt)
|
|
} else {
|
|
var match currency.Pair
|
|
match, err = incoming.DeriveFrom(pairNoFmt)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
diff.Remove, err = diff.Remove.Remove(enabledPairs[x])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
enabledPairs[target] = match.Format(pFmt)
|
|
}
|
|
target++
|
|
}
|
|
|
|
enabledPairs = enabledPairs[:target]
|
|
if len(enabledPairs) == 0 {
|
|
// NOTE: If enabled pairs are not populated for any reason.
|
|
var randomPair currency.Pair
|
|
randomPair, err = incoming.GetRandomPair()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
log.Debugf(log.ExchangeSys, "%s Enabled pairs missing for %s. Added %s.\n",
|
|
b.Name,
|
|
strings.ToUpper(a.String()),
|
|
randomPair)
|
|
enabledPairs = currency.Pairs{randomPair}
|
|
}
|
|
|
|
if len(diff.Remove) > 0 {
|
|
log.Debugf(log.ExchangeSys, "%s Checked and updated enabled pairs [%v] - Removed: %s.\n",
|
|
b.Name,
|
|
strings.ToUpper(a.String()),
|
|
diff.Remove)
|
|
}
|
|
err = b.Config.CurrencyPairs.StorePairs(a, enabledPairs, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return b.CurrencyPairs.StorePairs(a, enabledPairs, true)
|
|
}
|
|
|
|
// SetAPIURL sets configuration API URL for an exchange
|
|
func (b *Base) SetAPIURL() error {
|
|
checkInsecureEndpoint := func(endpoint string) {
|
|
if strings.Contains(endpoint, "https") || strings.Contains(endpoint, "wss") {
|
|
return
|
|
}
|
|
log.Warnf(log.ExchangeSys,
|
|
"%s is using HTTP instead of HTTPS or WS instead of WSS [%s] for API functionality, an"+
|
|
" attacker could eavesdrop on this connection. Use at your"+
|
|
" own risk.",
|
|
b.Name, endpoint)
|
|
}
|
|
var err error
|
|
if b.Config.API.OldEndPoints != nil {
|
|
if b.Config.API.OldEndPoints.URL != "" && b.Config.API.OldEndPoints.URL != config.APIURLNonDefaultMessage {
|
|
err = b.API.Endpoints.SetRunning(RestSpot.String(), b.Config.API.OldEndPoints.URL)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
checkInsecureEndpoint(b.Config.API.OldEndPoints.URL)
|
|
}
|
|
if b.Config.API.OldEndPoints.URLSecondary != "" && b.Config.API.OldEndPoints.URLSecondary != config.APIURLNonDefaultMessage {
|
|
err = b.API.Endpoints.SetRunning(RestSpotSupplementary.String(), b.Config.API.OldEndPoints.URLSecondary)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
checkInsecureEndpoint(b.Config.API.OldEndPoints.URLSecondary)
|
|
}
|
|
if b.Config.API.OldEndPoints.WebsocketURL != "" && b.Config.API.OldEndPoints.WebsocketURL != config.WebsocketURLNonDefaultMessage {
|
|
err = b.API.Endpoints.SetRunning(WebsocketSpot.String(), b.Config.API.OldEndPoints.WebsocketURL)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
checkInsecureEndpoint(b.Config.API.OldEndPoints.WebsocketURL)
|
|
}
|
|
b.Config.API.OldEndPoints = nil
|
|
} else if b.Config.API.Endpoints != nil {
|
|
for key, val := range b.Config.API.Endpoints {
|
|
if val == "" ||
|
|
val == config.APIURLNonDefaultMessage ||
|
|
val == config.WebsocketURLNonDefaultMessage {
|
|
continue
|
|
}
|
|
|
|
var u URL
|
|
u, err = getURLTypeFromString(key)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var defaultURL string
|
|
defaultURL, err = b.API.Endpoints.GetURL(u)
|
|
if err != nil {
|
|
log.Warnf(
|
|
log.ExchangeSys,
|
|
"%s: Config cannot match with default endpoint URL: [%s] with key: [%s], please remove or update core support endpoints.",
|
|
b.Name,
|
|
val,
|
|
u)
|
|
continue
|
|
}
|
|
|
|
if defaultURL == val {
|
|
continue
|
|
}
|
|
|
|
log.Warnf(
|
|
log.ExchangeSys,
|
|
"%s: Config is overwriting default endpoint URL values from: [%s] to: [%s] for: [%s]",
|
|
b.Name,
|
|
defaultURL,
|
|
val,
|
|
u)
|
|
|
|
checkInsecureEndpoint(val)
|
|
|
|
err = b.API.Endpoints.SetRunning(key, val)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
runningMap := b.API.Endpoints.GetURLMap()
|
|
b.Config.API.Endpoints = runningMap
|
|
return nil
|
|
}
|
|
|
|
// SupportsREST returns whether or not the exchange supports
|
|
// REST
|
|
func (b *Base) SupportsREST() bool {
|
|
return b.Features.Supports.REST
|
|
}
|
|
|
|
// GetWithdrawPermissions passes through the exchange's withdraw permissions
|
|
func (b *Base) GetWithdrawPermissions() uint32 {
|
|
return b.Features.Supports.WithdrawPermissions
|
|
}
|
|
|
|
// SupportsWithdrawPermissions compares the supplied permissions with the exchange's to verify they're supported
|
|
func (b *Base) SupportsWithdrawPermissions(permissions uint32) bool {
|
|
exchangePermissions := b.GetWithdrawPermissions()
|
|
return permissions&exchangePermissions == permissions
|
|
}
|
|
|
|
// FormatWithdrawPermissions will return each of the exchange's compatible withdrawal methods in readable form
|
|
func (b *Base) FormatWithdrawPermissions() string {
|
|
var services []string
|
|
for i := 0; i < 32; i++ {
|
|
var check uint32 = 1 << uint32(i)
|
|
if b.GetWithdrawPermissions()&check != 0 {
|
|
switch check {
|
|
case AutoWithdrawCrypto:
|
|
services = append(services, AutoWithdrawCryptoText)
|
|
case AutoWithdrawCryptoWithAPIPermission:
|
|
services = append(services, AutoWithdrawCryptoWithAPIPermissionText)
|
|
case AutoWithdrawCryptoWithSetup:
|
|
services = append(services, AutoWithdrawCryptoWithSetupText)
|
|
case WithdrawCryptoWith2FA:
|
|
services = append(services, WithdrawCryptoWith2FAText)
|
|
case WithdrawCryptoWithSMS:
|
|
services = append(services, WithdrawCryptoWithSMSText)
|
|
case WithdrawCryptoWithEmail:
|
|
services = append(services, WithdrawCryptoWithEmailText)
|
|
case WithdrawCryptoWithWebsiteApproval:
|
|
services = append(services, WithdrawCryptoWithWebsiteApprovalText)
|
|
case WithdrawCryptoWithAPIPermission:
|
|
services = append(services, WithdrawCryptoWithAPIPermissionText)
|
|
case AutoWithdrawFiat:
|
|
services = append(services, AutoWithdrawFiatText)
|
|
case AutoWithdrawFiatWithAPIPermission:
|
|
services = append(services, AutoWithdrawFiatWithAPIPermissionText)
|
|
case AutoWithdrawFiatWithSetup:
|
|
services = append(services, AutoWithdrawFiatWithSetupText)
|
|
case WithdrawFiatWith2FA:
|
|
services = append(services, WithdrawFiatWith2FAText)
|
|
case WithdrawFiatWithSMS:
|
|
services = append(services, WithdrawFiatWithSMSText)
|
|
case WithdrawFiatWithEmail:
|
|
services = append(services, WithdrawFiatWithEmailText)
|
|
case WithdrawFiatWithWebsiteApproval:
|
|
services = append(services, WithdrawFiatWithWebsiteApprovalText)
|
|
case WithdrawFiatWithAPIPermission:
|
|
services = append(services, WithdrawFiatWithAPIPermissionText)
|
|
case WithdrawCryptoViaWebsiteOnly:
|
|
services = append(services, WithdrawCryptoViaWebsiteOnlyText)
|
|
case WithdrawFiatViaWebsiteOnly:
|
|
services = append(services, WithdrawFiatViaWebsiteOnlyText)
|
|
case NoFiatWithdrawals:
|
|
services = append(services, NoFiatWithdrawalsText)
|
|
default:
|
|
services = append(services, fmt.Sprintf("%s[1<<%v]", UnknownWithdrawalTypeText, i))
|
|
}
|
|
}
|
|
}
|
|
if len(services) > 0 {
|
|
return strings.Join(services, " & ")
|
|
}
|
|
|
|
return NoAPIWithdrawalMethodsText
|
|
}
|
|
|
|
// SupportsAsset whether or not the supplied asset is supported
|
|
// by the exchange
|
|
func (b *Base) SupportsAsset(a asset.Item) bool {
|
|
_, ok := b.CurrencyPairs.Pairs[a]
|
|
return ok
|
|
}
|
|
|
|
// PrintEnabledPairs prints the exchanges enabled asset pairs
|
|
func (b *Base) PrintEnabledPairs() {
|
|
for k, v := range b.CurrencyPairs.Pairs {
|
|
log.Infof(log.ExchangeSys, "%s Asset type %v:\n\t Enabled pairs: %v",
|
|
b.Name, strings.ToUpper(k.String()), v.Enabled)
|
|
}
|
|
}
|
|
|
|
// GetBase returns the exchange base
|
|
func (b *Base) GetBase() *Base { return b }
|
|
|
|
// CheckTransientError catches transient errors and returns nil if found, used
|
|
// for validation of API credentials
|
|
func (b *Base) CheckTransientError(err error) error {
|
|
if _, ok := err.(net.Error); ok {
|
|
log.Warnf(log.ExchangeSys,
|
|
"%s net error captured, will not disable authentication %s",
|
|
b.Name,
|
|
err)
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
// DisableRateLimiter disables the rate limiting system for the exchange
|
|
func (b *Base) DisableRateLimiter() error {
|
|
return b.Requester.DisableRateLimiter()
|
|
}
|
|
|
|
// EnableRateLimiter enables the rate limiting system for the exchange
|
|
func (b *Base) EnableRateLimiter() error {
|
|
return b.Requester.EnableRateLimiter()
|
|
}
|
|
|
|
// StoreAssetPairFormat initialises and stores a defined asset format
|
|
func (b *Base) StoreAssetPairFormat(a asset.Item, f currency.PairStore) error {
|
|
if a.String() == "" {
|
|
return fmt.Errorf("%s cannot add to pairs manager, no asset provided",
|
|
b.Name)
|
|
}
|
|
|
|
if f.AssetEnabled == nil {
|
|
f.AssetEnabled = convert.BoolPtr(true)
|
|
}
|
|
|
|
if f.RequestFormat == nil {
|
|
return fmt.Errorf("%s cannot add to pairs manager, request pair format not provided",
|
|
b.Name)
|
|
}
|
|
|
|
if f.ConfigFormat == nil {
|
|
return fmt.Errorf("%s cannot add to pairs manager, config pair format not provided",
|
|
b.Name)
|
|
}
|
|
|
|
if f.ConfigFormat.Delimiter == "" {
|
|
return fmt.Errorf("exchange %s cannot set asset %s pair format %w",
|
|
b.Name, a, errConfigPairFormatRequiresDelimiter)
|
|
}
|
|
|
|
if b.CurrencyPairs.Pairs == nil {
|
|
b.CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore)
|
|
}
|
|
|
|
b.CurrencyPairs.Pairs[a] = &f
|
|
return nil
|
|
}
|
|
|
|
// SetGlobalPairsManager sets defined asset and pairs management system with
|
|
// global formatting
|
|
func (b *Base) SetGlobalPairsManager(request, config *currency.PairFormat, assets ...asset.Item) error {
|
|
if request == nil {
|
|
return fmt.Errorf("%s cannot set pairs manager, request pair format not provided",
|
|
b.Name)
|
|
}
|
|
|
|
if config == nil {
|
|
return fmt.Errorf("%s cannot set pairs manager, config pair format not provided",
|
|
b.Name)
|
|
}
|
|
|
|
if len(assets) == 0 {
|
|
return fmt.Errorf("%s cannot set pairs manager, no assets provided",
|
|
b.Name)
|
|
}
|
|
|
|
if config.Delimiter == "" {
|
|
return fmt.Errorf("exchange %s cannot set global pairs manager %w for assets %s",
|
|
b.Name, errConfigPairFormatRequiresDelimiter, assets)
|
|
}
|
|
|
|
b.CurrencyPairs.UseGlobalFormat = true
|
|
b.CurrencyPairs.RequestFormat = request
|
|
b.CurrencyPairs.ConfigFormat = config
|
|
|
|
if b.CurrencyPairs.Pairs != nil {
|
|
return fmt.Errorf("%s cannot set pairs manager, pairs already set",
|
|
b.Name)
|
|
}
|
|
|
|
b.CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore)
|
|
|
|
for i := range assets {
|
|
if assets[i].String() == "" {
|
|
b.CurrencyPairs.Pairs = nil
|
|
return fmt.Errorf("%s cannot set pairs manager, asset is empty string",
|
|
b.Name)
|
|
}
|
|
b.CurrencyPairs.Pairs[assets[i]] = new(currency.PairStore)
|
|
b.CurrencyPairs.Pairs[assets[i]].ConfigFormat = config
|
|
b.CurrencyPairs.Pairs[assets[i]].RequestFormat = request
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetWebsocket returns a pointer to the exchange websocket
|
|
func (b *Base) GetWebsocket() (*stream.Websocket, error) {
|
|
if b.Websocket == nil {
|
|
return nil, common.ErrFunctionNotSupported
|
|
}
|
|
return b.Websocket, nil
|
|
}
|
|
|
|
// SupportsWebsocket returns whether or not the exchange supports
|
|
// websocket
|
|
func (b *Base) SupportsWebsocket() bool {
|
|
return b.Features.Supports.Websocket
|
|
}
|
|
|
|
// IsWebsocketEnabled returns whether or not the exchange has its
|
|
// websocket client enabled
|
|
func (b *Base) IsWebsocketEnabled() bool {
|
|
if b.Websocket == nil {
|
|
return false
|
|
}
|
|
return b.Websocket.IsEnabled()
|
|
}
|
|
|
|
// FlushWebsocketChannels refreshes websocket channel subscriptions based on
|
|
// websocket features. Used in the event of a pair/asset or subscription change.
|
|
func (b *Base) FlushWebsocketChannels() error {
|
|
if b.Websocket == nil {
|
|
return nil
|
|
}
|
|
return b.Websocket.FlushChannels()
|
|
}
|
|
|
|
// SubscribeToWebsocketChannels appends to ChannelsToSubscribe
|
|
// which lets websocket.manageSubscriptions handle subscribing
|
|
func (b *Base) SubscribeToWebsocketChannels(channels []stream.ChannelSubscription) error {
|
|
if b.Websocket == nil {
|
|
return common.ErrFunctionNotSupported
|
|
}
|
|
return b.Websocket.SubscribeToChannels(channels)
|
|
}
|
|
|
|
// UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe
|
|
// which lets websocket.manageSubscriptions handle unsubscribing
|
|
func (b *Base) UnsubscribeToWebsocketChannels(channels []stream.ChannelSubscription) error {
|
|
if b.Websocket == nil {
|
|
return common.ErrFunctionNotSupported
|
|
}
|
|
return b.Websocket.UnsubscribeChannels(channels)
|
|
}
|
|
|
|
// GetSubscriptions returns a copied list of subscriptions
|
|
func (b *Base) GetSubscriptions() ([]stream.ChannelSubscription, error) {
|
|
if b.Websocket == nil {
|
|
return nil, common.ErrFunctionNotSupported
|
|
}
|
|
return b.Websocket.GetSubscriptions(), nil
|
|
}
|
|
|
|
// AuthenticateWebsocket sends an authentication message to the websocket
|
|
func (b *Base) AuthenticateWebsocket(_ context.Context) error {
|
|
return common.ErrFunctionNotSupported
|
|
}
|
|
|
|
// KlineIntervalEnabled returns if requested interval is enabled on exchange
|
|
func (b *Base) klineIntervalEnabled(in kline.Interval) bool {
|
|
// TODO: Add in the ability to use custom klines
|
|
return b.Features.Enabled.Kline.Intervals.ExchangeSupported(in)
|
|
}
|
|
|
|
// FormatExchangeKlineInterval returns Interval to string
|
|
// Exchanges can override this if they require custom formatting
|
|
func (b *Base) FormatExchangeKlineInterval(in kline.Interval) string {
|
|
return strconv.FormatFloat(in.Duration().Seconds(), 'f', 0, 64)
|
|
}
|
|
|
|
// ValidateKline confirms that the requested pair, asset & interval are
|
|
// supported and/or enabled by the requested exchange.
|
|
func (b *Base) ValidateKline(pair currency.Pair, a asset.Item, interval kline.Interval) error {
|
|
var errorList []string
|
|
if b.CurrencyPairs.IsAssetEnabled(a) != nil {
|
|
errorList = append(errorList, fmt.Sprintf("[%s] asset not enabled", a))
|
|
} else if !b.CurrencyPairs.Pairs[a].Enabled.Contains(pair, true) {
|
|
errorList = append(errorList, fmt.Sprintf("[%s] pair not enabled", pair))
|
|
}
|
|
|
|
if !b.klineIntervalEnabled(interval) {
|
|
errorList = append(errorList, fmt.Sprintf("[%s] interval not supported", interval))
|
|
}
|
|
|
|
if len(errorList) > 0 {
|
|
return fmt.Errorf("%w: %v", kline.ErrValidatingParams, strings.Join(errorList, ", "))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// AddTradesToBuffer is a helper function that will only
|
|
// add trades to the buffer if it is allowed
|
|
func (b *Base) AddTradesToBuffer(trades ...trade.Data) error {
|
|
if !b.IsSaveTradeDataEnabled() {
|
|
return nil
|
|
}
|
|
return trade.AddTradesToBuffer(b.Name, trades...)
|
|
}
|
|
|
|
// IsSaveTradeDataEnabled checks the state of
|
|
// SaveTradeData in a concurrent-friendly manner
|
|
func (b *Base) IsSaveTradeDataEnabled() bool {
|
|
b.settingsMutex.RLock()
|
|
isEnabled := b.Features.Enabled.SaveTradeData
|
|
b.settingsMutex.RUnlock()
|
|
return isEnabled
|
|
}
|
|
|
|
// SetSaveTradeDataStatus locks and sets the status of
|
|
// the config and the exchange's setting for SaveTradeData
|
|
func (b *Base) SetSaveTradeDataStatus(enabled bool) {
|
|
b.settingsMutex.Lock()
|
|
defer b.settingsMutex.Unlock()
|
|
b.Features.Enabled.SaveTradeData = enabled
|
|
b.Config.Features.Enabled.SaveTradeData = enabled
|
|
if b.Verbose {
|
|
log.Debugf(log.Trade, "Set %v 'SaveTradeData' to %v", b.Name, enabled)
|
|
}
|
|
}
|
|
|
|
// IsTradeFeedEnabled checks the state of
|
|
// TradeFeed in a concurrent-friendly manner
|
|
func (b *Base) IsTradeFeedEnabled() bool {
|
|
b.settingsMutex.RLock()
|
|
isEnabled := b.Features.Enabled.TradeFeed
|
|
b.settingsMutex.RUnlock()
|
|
return isEnabled
|
|
}
|
|
|
|
// SetTradeFeedStatus locks and sets the status of
|
|
// the config and the exchange's setting for TradeFeed
|
|
func (b *Base) SetTradeFeedStatus(enabled bool) {
|
|
b.settingsMutex.Lock()
|
|
defer b.settingsMutex.Unlock()
|
|
b.Features.Enabled.TradeFeed = enabled
|
|
b.Config.Features.Enabled.TradeFeed = enabled
|
|
if b.Verbose {
|
|
log.Debugf(log.Trade, "Set %v 'TradeFeed' to %v", b.Name, enabled)
|
|
}
|
|
}
|
|
|
|
// IsFillsFeedEnabled checks the state of
|
|
// FillsFeed in a concurrent-friendly manner
|
|
func (b *Base) IsFillsFeedEnabled() bool {
|
|
b.settingsMutex.RLock()
|
|
isEnabled := b.Features.Enabled.FillsFeed
|
|
b.settingsMutex.RUnlock()
|
|
return isEnabled
|
|
}
|
|
|
|
// SetFillsFeedStatus locks and sets the status of
|
|
// the config and the exchange's setting for FillsFeed
|
|
func (b *Base) SetFillsFeedStatus(enabled bool) {
|
|
b.settingsMutex.Lock()
|
|
defer b.settingsMutex.Unlock()
|
|
b.Features.Enabled.FillsFeed = enabled
|
|
b.Config.Features.Enabled.FillsFeed = enabled
|
|
if b.Verbose {
|
|
log.Debugf(log.Trade, "Set %v 'FillsFeed' to %v", b.Name, enabled)
|
|
}
|
|
}
|
|
|
|
// NewEndpoints declares default and running URLs maps
|
|
func (b *Base) NewEndpoints() *Endpoints {
|
|
return &Endpoints{
|
|
Exchange: b.Name,
|
|
defaults: make(map[string]string),
|
|
}
|
|
}
|
|
|
|
// SetDefaultEndpoints declares and sets the default URLs map
|
|
func (e *Endpoints) SetDefaultEndpoints(m map[URL]string) error {
|
|
for k, v := range m {
|
|
err := e.SetRunning(k.String(), v)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// SetRunning populates running URLs map
|
|
func (e *Endpoints) SetRunning(key, val string) error {
|
|
e.mu.Lock()
|
|
defer e.mu.Unlock()
|
|
err := validateKey(key)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = url.ParseRequestURI(val)
|
|
if err != nil {
|
|
log.Warnf(log.ExchangeSys,
|
|
"Could not set custom URL for %s to %s for exchange %s. invalid URI for request.",
|
|
key,
|
|
val,
|
|
e.Exchange)
|
|
return nil //nolint:nilerr // non-fatal error as we won't update the running URL
|
|
}
|
|
e.defaults[key] = val
|
|
return nil
|
|
}
|
|
|
|
func validateKey(keyVal string) error {
|
|
for x := range keyURLs {
|
|
if keyURLs[x].String() == keyVal {
|
|
return nil
|
|
}
|
|
}
|
|
return errors.New("keyVal invalid")
|
|
}
|
|
|
|
// GetURL gets default url from URLs map
|
|
func (e *Endpoints) GetURL(key URL) (string, error) {
|
|
e.mu.RLock()
|
|
defer e.mu.RUnlock()
|
|
val, ok := e.defaults[key.String()]
|
|
if !ok {
|
|
return "", fmt.Errorf("no endpoint path found for the given key: %v", key)
|
|
}
|
|
return val, nil
|
|
}
|
|
|
|
// GetURLMap gets all urls for either running or default map based on the bool value supplied
|
|
func (e *Endpoints) GetURLMap() map[string]string {
|
|
e.mu.RLock()
|
|
var urlMap = make(map[string]string)
|
|
for k, v := range e.defaults {
|
|
urlMap[k] = v
|
|
}
|
|
e.mu.RUnlock()
|
|
return urlMap
|
|
}
|
|
|
|
// FormatSymbol formats the given pair to a string suitable for exchange API requests
|
|
func (b *Base) FormatSymbol(pair currency.Pair, assetType asset.Item) (string, error) {
|
|
pairFmt, err := b.GetPairFormat(assetType, true)
|
|
if err != nil {
|
|
return pair.String(), err
|
|
}
|
|
return pairFmt.Format(pair), nil
|
|
}
|
|
|
|
func (u URL) String() string {
|
|
switch u {
|
|
case RestSpot:
|
|
return restSpotURL
|
|
case RestSpotSupplementary:
|
|
return restSpotSupplementaryURL
|
|
case RestUSDTMargined:
|
|
return restUSDTMarginedFuturesURL
|
|
case RestCoinMargined:
|
|
return restCoinMarginedFuturesURL
|
|
case RestFutures:
|
|
return restFuturesURL
|
|
case RestUSDCMargined:
|
|
return restUSDCMarginedFuturesURL
|
|
case RestSandbox:
|
|
return restSandboxURL
|
|
case RestSwap:
|
|
return restSwapURL
|
|
case WebsocketSpot:
|
|
return websocketSpotURL
|
|
case WebsocketSpotSupplementary:
|
|
return websocketSpotSupplementaryURL
|
|
case ChainAnalysis:
|
|
return chainAnalysisURL
|
|
case EdgeCase1:
|
|
return edgeCase1URL
|
|
case EdgeCase2:
|
|
return edgeCase2URL
|
|
case EdgeCase3:
|
|
return edgeCase3URL
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
// getURLTypeFromString returns URL type from the endpoint string association
|
|
func getURLTypeFromString(ep string) (URL, error) {
|
|
switch ep {
|
|
case restSpotURL:
|
|
return RestSpot, nil
|
|
case restSpotSupplementaryURL:
|
|
return RestSpotSupplementary, nil
|
|
case restUSDTMarginedFuturesURL:
|
|
return RestUSDTMargined, nil
|
|
case restCoinMarginedFuturesURL:
|
|
return RestCoinMargined, nil
|
|
case restFuturesURL:
|
|
return RestFutures, nil
|
|
case restUSDCMarginedFuturesURL:
|
|
return RestUSDCMargined, nil
|
|
case restSandboxURL:
|
|
return RestSandbox, nil
|
|
case restSwapURL:
|
|
return RestSwap, nil
|
|
case websocketSpotURL:
|
|
return WebsocketSpot, nil
|
|
case websocketSpotSupplementaryURL:
|
|
return WebsocketSpotSupplementary, nil
|
|
case chainAnalysisURL:
|
|
return ChainAnalysis, nil
|
|
case edgeCase1URL:
|
|
return EdgeCase1, nil
|
|
case edgeCase2URL:
|
|
return EdgeCase2, nil
|
|
case edgeCase3URL:
|
|
return EdgeCase3, nil
|
|
default:
|
|
return Invalid, fmt.Errorf("%w for %s", errEndpointStringNotFound, ep)
|
|
}
|
|
}
|
|
|
|
// UpdateOrderExecutionLimits updates order execution limits this is overridable
|
|
func (b *Base) UpdateOrderExecutionLimits(_ context.Context, _ asset.Item) error {
|
|
return common.ErrNotYetImplemented
|
|
}
|
|
|
|
// DisableAssetWebsocketSupport disables websocket functionality for the
|
|
// supplied asset item. In the case that websocket functionality has not yet
|
|
// been implemented for that specific asset type. This is a base method to
|
|
// check availability of asset type.
|
|
func (b *Base) DisableAssetWebsocketSupport(aType asset.Item) error {
|
|
if !b.SupportsAsset(aType) {
|
|
return fmt.Errorf("%s %w",
|
|
aType,
|
|
asset.ErrNotSupported)
|
|
}
|
|
b.AssetWebsocketSupport.m.Lock()
|
|
if b.AssetWebsocketSupport.unsupported == nil {
|
|
b.AssetWebsocketSupport.unsupported = make(map[asset.Item]bool)
|
|
}
|
|
b.AssetWebsocketSupport.unsupported[aType] = true
|
|
b.AssetWebsocketSupport.m.Unlock()
|
|
return nil
|
|
}
|
|
|
|
// IsAssetWebsocketSupported checks to see if the supplied asset type is
|
|
// supported by websocket.
|
|
func (a *AssetWebsocketSupport) IsAssetWebsocketSupported(aType asset.Item) bool {
|
|
a.m.RLock()
|
|
defer a.m.RUnlock()
|
|
return a.unsupported == nil || !a.unsupported[aType]
|
|
}
|
|
|
|
// UpdateCurrencyStates updates currency states
|
|
func (b *Base) UpdateCurrencyStates(ctx context.Context, a asset.Item) error {
|
|
return common.ErrNotYetImplemented
|
|
}
|
|
|
|
// GetAvailableTransferChains returns a list of supported transfer chains based
|
|
// on the supplied cryptocurrency
|
|
func (b *Base) GetAvailableTransferChains(_ context.Context, _ currency.Code) ([]string, error) {
|
|
return nil, common.ErrFunctionNotSupported
|
|
}
|
|
|
|
// CalculatePNL is an overridable function to allow PNL to be calculated on an
|
|
// open position
|
|
// It will also determine whether the position is considered to be liquidated
|
|
// For live trading, an overriding function may wish to confirm the liquidation by
|
|
// requesting the status of the asset
|
|
func (b *Base) CalculatePNL(context.Context, *order.PNLCalculatorRequest) (*order.PNLResult, error) {
|
|
return nil, common.ErrNotYetImplemented
|
|
}
|
|
|
|
// ScaleCollateral is an overridable function to determine how much
|
|
// collateral is usable in futures positions
|
|
func (b *Base) ScaleCollateral(context.Context, *order.CollateralCalculator) (*order.CollateralByCurrency, error) {
|
|
return nil, common.ErrNotYetImplemented
|
|
}
|
|
|
|
// CalculateTotalCollateral takes in n collateral calculators to determine an overall
|
|
// standing in a singular currency
|
|
func (b *Base) CalculateTotalCollateral(ctx context.Context, calculator *order.TotalCollateralCalculator) (*order.TotalCollateralResponse, error) {
|
|
return nil, common.ErrNotYetImplemented
|
|
}
|
|
|
|
// GetCollateralCurrencyForContract returns the collateral currency for an asset and contract pair
|
|
func (b *Base) GetCollateralCurrencyForContract(a asset.Item, cp currency.Pair) (currency.Code, asset.Item, error) {
|
|
return currency.Code{}, asset.Empty, common.ErrNotYetImplemented
|
|
}
|
|
|
|
// GetCurrencyForRealisedPNL returns where to put realised PNL
|
|
// example 1: Bybit universal margin PNL is paid out in USD to your spot wallet
|
|
// example 2: Binance coin margined futures pays returns using the same currency eg BTC
|
|
func (b *Base) GetCurrencyForRealisedPNL(_ asset.Item, _ currency.Pair) (currency.Code, asset.Item, error) {
|
|
return currency.Code{}, asset.Empty, common.ErrNotYetImplemented
|
|
}
|
|
|
|
// HasAssetTypeAccountSegregation returns if the accounts are divided into asset
|
|
// types instead of just being denoted as spot holdings.
|
|
func (b *Base) HasAssetTypeAccountSegregation() bool {
|
|
return b.Features.Supports.RESTCapabilities.HasAssetTypeAccountSegregation
|
|
}
|
|
|
|
// GetServerTime returns the current exchange server time.
|
|
func (b *Base) GetServerTime(context.Context, asset.Item) (time.Time, error) {
|
|
return time.Time{}, common.ErrNotYetImplemented
|
|
}
|
|
|
|
// GetMarginRatesHistory returns the margin rate history for the supplied currency
|
|
func (b *Base) GetMarginRatesHistory(context.Context, *margin.RateHistoryRequest) (*margin.RateHistoryResponse, error) {
|
|
return nil, common.ErrNotYetImplemented
|
|
}
|
|
|
|
// GetPositionSummary returns stats for a future position
|
|
func (b *Base) GetPositionSummary(context.Context, *order.PositionSummaryRequest) (*order.PositionSummary, error) {
|
|
return nil, common.ErrNotYetImplemented
|
|
}
|
|
|
|
// GetFundingPaymentDetails returns funding payment details for a future for a specific time period
|
|
func (b *Base) GetFundingPaymentDetails(context.Context, *order.FundingRatesRequest) (*order.FundingRates, error) {
|
|
return nil, common.ErrNotYetImplemented
|
|
}
|
|
|
|
// GetFuturesPositions returns futures positions for all currencies
|
|
func (b *Base) GetFuturesPositions(context.Context, *order.PositionsRequest) ([]order.PositionDetails, error) {
|
|
return nil, common.ErrNotYetImplemented
|
|
}
|
|
|
|
// GetFundingRates returns funding rates based on request data
|
|
func (b *Base) GetFundingRates(context.Context, *order.FundingRatesRequest) ([]order.FundingRates, error) {
|
|
return nil, common.ErrNotYetImplemented
|
|
}
|
|
|
|
// IsPerpetualFutureCurrency ensures a given asset and currency is a perpetual future
|
|
// differs by exchange
|
|
func (b *Base) IsPerpetualFutureCurrency(asset.Item, currency.Pair) (bool, error) {
|
|
return false, common.ErrNotYetImplemented
|
|
}
|
|
|
|
// GetKlineRequest returns a helper for the fetching of candle/kline data for
|
|
// a single request within a pre-determined time window.
|
|
func (b *Base) GetKlineRequest(pair currency.Pair, a asset.Item, interval kline.Interval, start, end time.Time, fixedAPICandleLength bool) (*kline.Request, error) {
|
|
if pair.IsEmpty() {
|
|
return nil, currency.ErrCurrencyPairEmpty
|
|
}
|
|
if !a.IsValid() {
|
|
return nil, asset.ErrNotSupported
|
|
}
|
|
|
|
// NOTE: This allows for checking that the required kline interval is
|
|
// supported by the exchange and/or can be constructed from lower time frame
|
|
// intervals.
|
|
exchangeInterval, err := b.Features.Enabled.Kline.Intervals.Construct(interval)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = b.ValidateKline(pair, a, exchangeInterval)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
formatted, err := b.FormatExchangeCurrency(pair, a)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
limit, err := b.Features.Enabled.Kline.GetIntervalResultLimit(exchangeInterval)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
req, err := kline.CreateKlineRequest(b.Name, pair, formatted, a, interval, exchangeInterval, start, end, limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// NOTE: The checks below makes sure a client is notified that using this
|
|
// functionality will result in error if the total candles cannot be
|
|
// theoretically retrieved.
|
|
if fixedAPICandleLength {
|
|
origCount := kline.TotalCandlesPerInterval(req.Start, req.End, interval)
|
|
modifiedCount := kline.TotalCandlesPerInterval(req.Start, time.Now(), exchangeInterval)
|
|
if modifiedCount > limit {
|
|
errMsg := fmt.Sprintf("for %v %v candles between %v-%v. ",
|
|
origCount,
|
|
interval,
|
|
start.Format(common.SimpleTimeFormatWithTimezone),
|
|
end.Format(common.SimpleTimeFormatWithTimezone))
|
|
if interval != exchangeInterval {
|
|
errMsg += fmt.Sprintf("Request converts to %v %v candles. ",
|
|
modifiedCount,
|
|
exchangeInterval)
|
|
}
|
|
boundary := time.Now().Add(-exchangeInterval.Duration() * time.Duration(limit))
|
|
return nil, fmt.Errorf("%w %v, exceeding the limit of %v %v candles up to %v. Please reduce timeframe or use GetHistoricCandlesExtended",
|
|
kline.ErrRequestExceedsExchangeLimits,
|
|
errMsg,
|
|
limit,
|
|
exchangeInterval,
|
|
boundary.Format(common.SimpleTimeFormatWithTimezone))
|
|
}
|
|
} else if count := kline.TotalCandlesPerInterval(req.Start, req.End, exchangeInterval); count > limit {
|
|
return nil, fmt.Errorf("candle count exceeded: %d. The endpoint has a set candle limit return of %d candles. Candle data will be incomplete: %w",
|
|
count,
|
|
limit,
|
|
kline.ErrRequestExceedsExchangeLimits)
|
|
}
|
|
|
|
return req, nil
|
|
}
|
|
|
|
// GetKlineExtendedRequest returns a helper for the fetching of candle/kline
|
|
// data for a *multi* request within a pre-determined time window. This has
|
|
// extended functionality to also break down calls to fetch total history.
|
|
func (b *Base) GetKlineExtendedRequest(pair currency.Pair, a asset.Item, interval kline.Interval, start, end time.Time) (*kline.ExtendedRequest, error) {
|
|
if pair.IsEmpty() {
|
|
return nil, currency.ErrCurrencyPairEmpty
|
|
}
|
|
if !a.IsValid() {
|
|
return nil, asset.ErrNotSupported
|
|
}
|
|
|
|
exchangeInterval, err := b.Features.Enabled.Kline.Intervals.Construct(interval)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = b.ValidateKline(pair, a, exchangeInterval)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
formatted, err := b.FormatExchangeCurrency(pair, a)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
limit, err := b.Features.Enabled.Kline.GetIntervalResultLimit(exchangeInterval)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
r, err := kline.CreateKlineRequest(b.Name, pair, formatted, a, interval, exchangeInterval, start, end, limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
r.IsExtended = true
|
|
|
|
dates, err := r.GetRanges(uint32(limit))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &kline.ExtendedRequest{Request: r, RangeHolder: dates}, nil
|
|
}
|
|
|
|
// Shutdown closes active websocket connections if available and then cleans up
|
|
// a REST requester instance.
|
|
func (b *Base) Shutdown() error {
|
|
if b.Websocket != nil {
|
|
err := b.Websocket.Shutdown()
|
|
if err != nil && !errors.Is(err, stream.ErrNotConnected) {
|
|
return err
|
|
}
|
|
}
|
|
return b.Requester.Shutdown()
|
|
}
|