Files
gocryptotrader/exchanges/exchange.go
Scott 80bc8c7e9e Trade history, recent trades, live trade processing and storage (#558)
* End of day commit moving packages and setting foundation into how trade processing will go

* Conformity

* tdd candle generation based on received trade data, renames orderbookbuffer back to buffer for now...

* Formalises test functions and designs the trade processor

* Theoretical amending old candles to allow any trades that were part of an old processed candle to be more accurate. Saving of candles will only occur on previous cycles, extending memory usage a bit longer

* Figures out sqlboiler for sqlite. Updates websocket entries to process trade data

* One more trade data

* Adds more exchange support

* Adds PSQL stuff

* Begins creating sql implementation

* End of day commit. Helper functions and understanding sql usage in GCT

* Adds delete and cleans up table design

* Finishes trades conceptually. Awaits candle data update in order to translate trades to candles

* Initial handling of trades in coinbene

* Proto

* Fixing of some bugs, attempting to address coinbene asset type ws issues

* Fixes up coinbene websocket implementation for the most part

* finalises coinbene websocket implementation. Adds new ability to parse currencies without a delimiter

* Implements rpc commands and adds testing

* updates the following to be compatible with trade data update: Theoretical amending old candles to allow any trades that were part of an old processed candle to be more accurate. Saving of candles will only occur on previous cycles, extending memory usage a bit longer

* Changes trade to be its own entity rather than attached to a websocket.

* Adds coverage to trades. Changes signature of `AddTradesToBuffer` to return error. Now automatically shuts down without need for channel listening. Will automatically start up again if it gets data

* Implements trade fetching at the wrapper level for a bunch of exchanges. Adds trade id to script updoot. Probably breaking change

* Implements trade fetching for all wrappers hurray hurrah. Updates all the tests

* Adds new interface func to get recent trades. Ensures GetExchangeHistory continues until conditions are met

* Adds new readme, tests all new wrapper endpoints, updates exchange_wrapper_issues to test new endpoints. Updates exchange_wrapper_coverage with new coverage... Fixes lame bug causing wrapper tests to fail from being poorly setup. Adds loopy loop to ensure that all data is captured when requesting exchange history

* Bugfix on psql migrations. Rebases latest changes, updates table design to use base and quote, updates trades to use exchange_name_id

* Adds new config field for saving trades to the database per exchange. Now exits trade processing when trade saving is not enabled. Similarly for wrapper, does not save if not enabled

* Minor bitfinex trade fixes. continues on buffer processing errors, now saves transactionid to the db

* Adds support for generating candles from candlesextended. May extend it further, idk

* Updates trade candles to be able to fill missing data with trades. Adds more tests. Also does a thing where you can forcefully override a candle based on internal trade data instead of API data

* Fixes bug where force deletions did not follow up with insertions. Adds force to candle commands

* Fixes specific exchange based issues. Extends recent trades to 24 hours where possible

* Fixes issue with saved tests. Fixes tests for trades. Adds parallel to tests. Pre-fixes people's nits

* Adds new GRPC functions to find out what data is missing from trades and candles. Fixes some assumptions from missing period code.

* Adds unique constraint. Fixes up niggling issues for wrappers and websockets

* Fixes issues with using unix times in the database trying to retrieve data via the CLI. Reduces save time to 15 seconds

* Updates trades to use timestamps instead of int64 unix

* Adds missing FTX wrapper implementation. Regens docs

* Linting the linters. Updating readme

* Adds new command to set whether an exchange can process trades

* Doc update

* Adds recent trades and historic trade endpoints to grpc

* formats pair_test.go to appease linter gods

* Addresses data race. Removes logging of missing intervals on unrelated function (now that it has its own rpc command). The buffer time isnt customisable, but I don't feel it needs to be at a config level at all really.

* Fixes a few niterinos regarding spacing, type conversion, a weird Bitmex 0 trade value error, unsubscriptions and cli command references

* Reduces map lookups. Adds base func and moves wrappers to use it

* Uses better currency formatter. Adds time based validation to trade history. Reverts configtest.json

* Reverts config and updates test names. Also WAYYYYY LESS SPAMMY

* oopsie doopsie missed a whoopsie

* mint flavoured lint

* Fixes issues caused by rebase

* Fixes issue with timestamps not converting properly from command to RPCServer. Adds new error type. Adds shorthand entries to some commands. Removes os.Exit from tests. Makes Gemini test rolling. Adds enabled exchange check to RPC function. Escapes timestamp on bitstamp. Renames var

* fixes whoopsie oopsie doopsie I forgot to remove code shoopsie

* missed a line

* 🎉 🎉 :tada:Breaks everything in an end of day commit 🎉 🎉 🎉

* Modifies function 'createlocaloffset' to return a string instead. Uses strings for all time based start and end commands. Uses UTC times in RPC server and updates SQLITE to use formatted time based queries

* Adds concurrency-safe way of changing SaveTradeData and checking it. Fixes embarrassing typo

* End of day fix, adds bitfinex update to loop until either the return trades shows no new dates, or meets specifications. Fixes egregious typo

* Improves testing and handling of historical trades function

* Fixes tests after latest changes

* Fix potential fatal err now that db is enabled in test config now

* Fixes up some database settings to use a local engine instead of global var

* DELICIOUS LINT CHOCOLATE FIXES

* Fixes data race by slashing competitor's tyres

* Adds mock test fixes to allow for live and stored data test

* Removes verbosity in engine level tests. Adds new timezone format to highlight the timezone for RPC functions. Removes reference to Preix index fund

* Oopsie doopsie, fixed a whoopsie

* Loggers can no longer do data drag races on my lawn 👴

* Removes bad lock

* Addresses command nits. End of day conceptual commit, trying to calculate spans of time in the context of missing periods. Tests will fail

* Adds new stream response for retrieving trade history as it can take time to do. Unsuccessfully attempts to simplify time range calculation for missing trades response

* Adds new timeperiods package to calculate time periods, time ranges and whether data is in those ranges. Removes kline basic implementation of same concept

* Fixes lint issues. Fixes test. Moves trade cli commands to their own trade subcommands

* Updates lakebtc to no longer have gethistorictrades as it is unsupported. Adds more validation to rpc functions

* Removes requirement to have trades when testing trade wrapper functions. Doesn't really prove it works if there are no trades for a given currency in a time period.

* Addresses nits, runs linting fix and ensures a test is consistent

* Fix merge issues

* Moves sort to timeperiods. Adds test coverage. Fixes typo

* Removes log package in CLI

* Fixes `GetTrades` url

* Reorders all instances of validation occuring after settingup RPC connection

* Fixes test to ensure that it is setup before testing that it is setup

* Fixed issue with bool retrieval. Removes double append

* Fixes Binance times, fixes bitfinex sell sides, fixes huobi times, sorts all responses

* Fixes poloniex trade id consistency. Makes recent trade for poloniex consistent with others (15 minutes). Fixes coinbene. Fixes localbitcoins to use quote currency. Fixes coinut times. Updates huobi trade id, saves okgroup trades. Fixes bid and ask to buy and sell

* Removes websocket trades for lakebtc as it did not meet our requirements for processing. Adds new constraints to the database to ensure we have uniqueness on trades where ID doesn't exist and doesn't trigger errors for trades where the tid does

* Fixes migration for postgres to downscale properly

* Really really fixes the psql index changes

* Fixes broken tests

* Now with working tests and no pocket lint

* Makes the side column nullable with no more constraint for it. adds migrations and runs generation. comments lakebtc

* Lint & Sprüngli

* Updates zb to use more appropriate side

* Fixes oopsie

* Attempts to address a data race from globals

* Fixes build

* Fixes missed regen rpc files

* Updates readme to point to trade readme. Fixes exchange_wrapper_coverage wrapper count and untested panics, tests bitfinex funding pair test for `fUSD`, adds shiny new param `tradeprocessinginterval`

* mint flavoured lint

* Uses the real default to set the default value by default

* Fixes some extra tests surrounding email sending and number incompatibility

* Reverts test config

* re-adds gom2/usdt currency

* Fixes typo, don't look!

* Fixes minor codelingo pickups

* Adds more precision to handling of trade data from Kraken. Expands test

* interface christmas tree

* lint
2020-10-29 13:00:02 +11:00

1138 lines
35 KiB
Go

package exchange
import (
"errors"
"fmt"
"net"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/common/crypto"
"github.com/thrasher-corp/gocryptotrader/config"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
"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)
// WarningAuthenticatedRequestWithoutCredentialsSet error message for authenticated request without credentials set
WarningAuthenticatedRequestWithoutCredentialsSet = "exchange %s authenticated HTTP request called but not supported due to unset/default API keys"
// 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
)
func (e *Base) checkAndInitRequester() {
if e.Requester == nil {
e.Requester = request.New(e.Name,
&http.Client{Transport: new(http.Transport)})
}
}
// SetHTTPClientTimeout sets the timeout value for the exchanges HTTP Client and
// also the underlying transports idle connection timeout
func (e *Base) SetHTTPClientTimeout(t time.Duration) error {
e.checkAndInitRequester()
e.Requester.HTTPClient.Timeout = t
tr, ok := e.Requester.HTTPClient.Transport.(*http.Transport)
if !ok {
return errors.New("transport not set, cannot set timeout")
}
tr.IdleConnTimeout = t
return nil
}
// SetHTTPClient sets exchanges HTTP client
func (e *Base) SetHTTPClient(h *http.Client) {
e.checkAndInitRequester()
e.Requester.HTTPClient = h
}
// GetHTTPClient gets the exchanges HTTP client
func (e *Base) GetHTTPClient() *http.Client {
e.checkAndInitRequester()
return e.Requester.HTTPClient
}
// SetHTTPClientUserAgent sets the exchanges HTTP user agent
func (e *Base) SetHTTPClientUserAgent(ua string) {
e.checkAndInitRequester()
e.Requester.UserAgent = ua
e.HTTPUserAgent = ua
}
// GetHTTPClientUserAgent gets the exchanges HTTP user agent
func (e *Base) GetHTTPClientUserAgent() string {
return e.HTTPUserAgent
}
// SetClientProxyAddress sets a proxy address for REST and websocket requests
func (e *Base) SetClientProxyAddress(addr string) error {
if addr == "" {
return nil
}
proxy, err := url.Parse(addr)
if err != nil {
return fmt.Errorf("exchange.go - setting proxy address error %s",
err)
}
err = e.Requester.SetProxy(proxy)
if err != nil {
return err
}
if e.Websocket != nil {
err = e.Websocket.SetProxyAddress(addr)
if err != nil {
return err
}
}
return nil
}
// SetFeatureDefaults sets the exchanges default feature
// support set
func (e *Base) SetFeatureDefaults() {
if e.Config.Features == nil {
s := &config.FeaturesConfig{
Supports: config.FeaturesSupportedConfig{
Websocket: e.Features.Supports.Websocket,
REST: e.Features.Supports.REST,
RESTCapabilities: protocol.Features{
AutoPairUpdates: e.Features.Supports.RESTCapabilities.AutoPairUpdates,
},
},
}
if e.Config.SupportsAutoPairUpdates != nil {
s.Supports.RESTCapabilities.AutoPairUpdates = *e.Config.SupportsAutoPairUpdates
s.Enabled.AutoPairUpdates = *e.Config.SupportsAutoPairUpdates
} else {
s.Supports.RESTCapabilities.AutoPairUpdates = e.Features.Supports.RESTCapabilities.AutoPairUpdates
s.Enabled.AutoPairUpdates = e.Features.Supports.RESTCapabilities.AutoPairUpdates
if !s.Supports.RESTCapabilities.AutoPairUpdates {
e.Config.CurrencyPairs.LastUpdated = time.Now().Unix()
e.CurrencyPairs.LastUpdated = e.Config.CurrencyPairs.LastUpdated
}
}
e.Config.Features = s
e.Config.SupportsAutoPairUpdates = nil
} else {
if e.Features.Supports.RESTCapabilities.AutoPairUpdates != e.Config.Features.Supports.RESTCapabilities.AutoPairUpdates {
e.Config.Features.Supports.RESTCapabilities.AutoPairUpdates = e.Features.Supports.RESTCapabilities.AutoPairUpdates
if !e.Config.Features.Supports.RESTCapabilities.AutoPairUpdates {
e.Config.CurrencyPairs.LastUpdated = time.Now().Unix()
}
}
if e.Features.Supports.REST != e.Config.Features.Supports.REST {
e.Config.Features.Supports.REST = e.Features.Supports.REST
}
if e.Features.Supports.RESTCapabilities.TickerBatching != e.Config.Features.Supports.RESTCapabilities.TickerBatching {
e.Config.Features.Supports.RESTCapabilities.TickerBatching = e.Features.Supports.RESTCapabilities.TickerBatching
}
if e.Features.Supports.Websocket != e.Config.Features.Supports.Websocket {
e.Config.Features.Supports.Websocket = e.Features.Supports.Websocket
}
if e.IsSaveTradeDataEnabled() != e.Config.Features.Enabled.SaveTradeData {
e.SetSaveTradeDataStatus(e.Config.Features.Enabled.SaveTradeData)
}
e.Features.Enabled.AutoPairUpdates = e.Config.Features.Enabled.AutoPairUpdates
}
}
// SetAPICredentialDefaults sets the API Credential validator defaults
func (e *Base) SetAPICredentialDefaults() {
// Exchange hardcoded settings take precedence and overwrite the config settings
if e.Config.API.CredentialsValidator == nil {
e.Config.API.CredentialsValidator = new(config.APICredentialsValidatorConfig)
}
if e.Config.API.CredentialsValidator.RequiresKey != e.API.CredentialsValidator.RequiresKey {
e.Config.API.CredentialsValidator.RequiresKey = e.API.CredentialsValidator.RequiresKey
}
if e.Config.API.CredentialsValidator.RequiresSecret != e.API.CredentialsValidator.RequiresSecret {
e.Config.API.CredentialsValidator.RequiresSecret = e.API.CredentialsValidator.RequiresSecret
}
if e.Config.API.CredentialsValidator.RequiresBase64DecodeSecret != e.API.CredentialsValidator.RequiresBase64DecodeSecret {
e.Config.API.CredentialsValidator.RequiresBase64DecodeSecret = e.API.CredentialsValidator.RequiresBase64DecodeSecret
}
if e.Config.API.CredentialsValidator.RequiresClientID != e.API.CredentialsValidator.RequiresClientID {
e.Config.API.CredentialsValidator.RequiresClientID = e.API.CredentialsValidator.RequiresClientID
}
if e.Config.API.CredentialsValidator.RequiresPEM != e.API.CredentialsValidator.RequiresPEM {
e.Config.API.CredentialsValidator.RequiresPEM = e.API.CredentialsValidator.RequiresPEM
}
}
// SupportsRESTTickerBatchUpdates returns whether or not the
// exhange supports REST batch ticker fetching
func (e *Base) SupportsRESTTickerBatchUpdates() bool {
return e.Features.Supports.RESTCapabilities.TickerBatching
}
// SupportsAutoPairUpdates returns whether or not the exchange supports
// auto currency pair updating
func (e *Base) SupportsAutoPairUpdates() bool {
if e.Features.Supports.RESTCapabilities.AutoPairUpdates ||
e.Features.Supports.WebsocketCapabilities.AutoPairUpdates {
return true
}
return false
}
// GetLastPairsUpdateTime returns the unix timestamp of when the exchanges
// currency pairs were last updated
func (e *Base) GetLastPairsUpdateTime() int64 {
return e.CurrencyPairs.LastUpdated
}
// GetAssetTypes returns the available asset types for an individual exchange
func (e *Base) GetAssetTypes() asset.Items {
return e.CurrencyPairs.GetAssetTypes()
}
// 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 (e *Base) GetPairAssetType(c currency.Pair) (asset.Item, error) {
assetTypes := e.GetAssetTypes()
for i := range assetTypes {
avail, err := e.GetAvailablePairs(assetTypes[i])
if err != nil {
return "", err
}
if avail.Contains(c, true) {
return assetTypes[i], nil
}
}
return "", errors.New("asset type not associated with currency pair")
}
// GetClientBankAccounts returns banking details associated with
// a client for withdrawal purposes
func (e *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 (e *Base) GetExchangeBankAccounts(id, depositCurrency string) (*banking.Account, error) {
cfg := config.GetConfig()
return cfg.GetExchangeBankAccounts(e.Name, id, depositCurrency)
}
// SetCurrencyPairFormat checks the exchange request and config currency pair
// formats and syncs it with the exchanges SetDefault settings
func (e *Base) SetCurrencyPairFormat() {
if e.Config.CurrencyPairs == nil {
e.Config.CurrencyPairs = new(currency.PairsManager)
}
e.Config.CurrencyPairs.UseGlobalFormat = e.CurrencyPairs.UseGlobalFormat
if e.Config.CurrencyPairs.UseGlobalFormat {
e.Config.CurrencyPairs.RequestFormat = e.CurrencyPairs.RequestFormat
e.Config.CurrencyPairs.ConfigFormat = e.CurrencyPairs.ConfigFormat
return
}
if e.Config.CurrencyPairs.ConfigFormat != nil {
e.Config.CurrencyPairs.ConfigFormat = nil
}
if e.Config.CurrencyPairs.RequestFormat != nil {
e.Config.CurrencyPairs.RequestFormat = nil
}
assetTypes := e.GetAssetTypes()
for x := range assetTypes {
if _, err := e.Config.CurrencyPairs.Get(assetTypes[x]); err != nil {
ps, err := e.CurrencyPairs.Get(assetTypes[x])
if err != nil {
continue
}
e.Config.CurrencyPairs.Store(assetTypes[x], *ps)
}
}
}
// SetConfigPairs sets the exchanges currency pairs to the pairs set in the config
func (e *Base) SetConfigPairs() error {
assetTypes := e.Config.CurrencyPairs.GetAssetTypes()
exchangeAssets := e.CurrencyPairs.GetAssetTypes()
for x := range assetTypes {
if !exchangeAssets.Contains(assetTypes[x]) {
log.Warnf(log.ExchangeSys,
"%s exchange asset type %s unsupported, please manually remove from configuration",
e.Name,
assetTypes[x])
}
cfgPS, err := e.Config.CurrencyPairs.Get(assetTypes[x])
if err != nil {
return err
}
var enabledAsset bool
if e.Config.CurrencyPairs.IsAssetEnabled(assetTypes[x]) == nil {
enabledAsset = true
}
e.CurrencyPairs.SetAssetEnabled(assetTypes[x], enabledAsset)
if e.Config.CurrencyPairs.UseGlobalFormat {
e.CurrencyPairs.StorePairs(assetTypes[x], cfgPS.Available, false)
e.CurrencyPairs.StorePairs(assetTypes[x], cfgPS.Enabled, true)
continue
}
exchPS, err := e.CurrencyPairs.Get(assetTypes[x])
if err != nil {
return err
}
cfgPS.ConfigFormat = exchPS.ConfigFormat
cfgPS.RequestFormat = exchPS.RequestFormat
e.CurrencyPairs.StorePairs(assetTypes[x], cfgPS.Available, false)
e.CurrencyPairs.StorePairs(assetTypes[x], cfgPS.Enabled, true)
}
return nil
}
// GetAuthenticatedAPISupport returns whether the exchange supports
// authenticated API requests
func (e *Base) GetAuthenticatedAPISupport(endpoint uint8) bool {
switch endpoint {
case RestAuthentication:
return e.API.AuthenticatedSupport
case WebsocketAuthentication:
return e.API.AuthenticatedWebsocketSupport
}
return false
}
// GetName is a method that returns the name of the exchange base
func (e *Base) GetName() string {
return e.Name
}
// GetEnabledFeatures returns the exchanges enabled features
func (e *Base) GetEnabledFeatures() FeaturesEnabled {
return e.Features.Enabled
}
// GetSupportedFeatures returns the exchanges supported features
func (e *Base) GetSupportedFeatures() FeaturesSupported {
return e.Features.Supports
}
// GetPairFormat returns the pair format based on the exchange and
// asset type
func (e *Base) GetPairFormat(assetType asset.Item, requestFormat bool) (currency.PairFormat, error) {
if e.CurrencyPairs.UseGlobalFormat {
if requestFormat {
if e.CurrencyPairs.RequestFormat == nil {
return currency.PairFormat{},
errors.New("global request format is nil")
}
return *e.CurrencyPairs.RequestFormat, nil
}
if e.CurrencyPairs.ConfigFormat == nil {
return currency.PairFormat{},
errors.New("global config format is nil")
}
return *e.CurrencyPairs.ConfigFormat, nil
}
ps, err := e.CurrencyPairs.Get(assetType)
if err != nil {
return currency.PairFormat{}, err
}
if requestFormat {
if ps.RequestFormat == nil {
return currency.PairFormat{},
errors.New("asset type request format is nil")
}
return *ps.RequestFormat, nil
}
if ps.ConfigFormat == nil {
return currency.PairFormat{},
errors.New("asset type config format is nil")
}
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 (e *Base) GetEnabledPairs(a asset.Item) (currency.Pairs, error) {
err := e.CurrencyPairs.IsAssetEnabled(a)
if err != nil {
return nil, nil
}
format, err := e.GetPairFormat(a, false)
if err != nil {
return nil, err
}
enabledpairs, err := e.CurrencyPairs.GetPairs(a, true)
if err != nil {
return nil, err
}
return enabledpairs.Format(format.Delimiter,
format.Index,
format.Uppercase),
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 (e *Base) GetRequestFormattedPairAndAssetType(p string) (currency.Pair, asset.Item, error) {
assetTypes := e.GetAssetTypes()
var response currency.Pair
for i := range assetTypes {
format, err := e.GetPairFormat(assetTypes[i], true)
if err != nil {
return response, assetTypes[i], err
}
pairs, err := e.CurrencyPairs.GetPairs(assetTypes[i], true)
if err != nil {
return response, assetTypes[i], err
}
for j := range pairs {
formattedPair := pairs[j].Format(format.Delimiter, format.Uppercase)
if strings.EqualFold(formattedPair.String(), p) {
return formattedPair, assetTypes[i], nil
}
}
}
return response, "", errors.New("pair not found: " + p)
}
// GetAvailablePairs is a method that returns the available currency pairs
// of the exchange by asset type
func (e *Base) GetAvailablePairs(assetType asset.Item) (currency.Pairs, error) {
format, err := e.GetPairFormat(assetType, false)
if err != nil {
return nil, err
}
pairs, err := e.CurrencyPairs.GetPairs(assetType, false)
if err != nil {
return nil, err
}
return pairs.Format(format.Delimiter, format.Index, format.Uppercase), nil
}
// SupportsPair returns true or not whether a currency pair exists in the
// exchange available currencies or not
func (e *Base) SupportsPair(p currency.Pair, enabledPairs bool, assetType asset.Item) error {
if enabledPairs {
pairs, err := e.GetEnabledPairs(assetType)
if err != nil {
return err
}
if pairs.Contains(p, false) {
return nil
}
return errors.New("pair not supported")
}
avail, err := e.GetAvailablePairs(assetType)
if err != nil {
return err
}
if avail.Contains(p, false) {
return nil
}
return errors.New("pair not supported")
}
// FormatExchangeCurrencies returns a string containing
// the exchanges formatted currency pairs
func (e *Base) FormatExchangeCurrencies(pairs []currency.Pair, assetType asset.Item) (string, error) {
var currencyItems strings.Builder
pairFmt, err := e.GetPairFormat(assetType, true)
if err != nil {
return "", err
}
for x := range pairs {
format, err := e.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 (e *Base) FormatExchangeCurrency(p currency.Pair, assetType asset.Item) (currency.Pair, error) {
pairFmt, err := e.GetPairFormat(assetType, true)
if err != nil {
return currency.Pair{}, err
}
return p.Format(pairFmt.Delimiter, pairFmt.Uppercase), nil
}
// SetEnabled is a method that sets if the exchange is enabled
func (e *Base) SetEnabled(enabled bool) {
e.Enabled = enabled
}
// IsEnabled is a method that returns if the current exchange is enabled
func (e *Base) IsEnabled() bool {
return e.Enabled
}
// SetAPIKeys is a method that sets the current API keys for the exchange
func (e *Base) SetAPIKeys(apiKey, apiSecret, clientID string) {
e.API.Credentials.Key = apiKey
e.API.Credentials.ClientID = clientID
if e.API.CredentialsValidator.RequiresBase64DecodeSecret {
result, err := crypto.Base64Decode(apiSecret)
if err != nil {
e.API.AuthenticatedSupport = false
e.API.AuthenticatedWebsocketSupport = false
log.Warnf(log.ExchangeSys,
warningBase64DecryptSecretKeyFailed,
e.Name)
return
}
e.API.Credentials.Secret = string(result)
} else {
e.API.Credentials.Secret = apiSecret
}
}
// SetupDefaults sets the exchange settings based on the supplied config
func (e *Base) SetupDefaults(exch *config.ExchangeConfig) error {
e.Enabled = true
e.LoadedByConfig = true
e.Config = exch
e.Verbose = exch.Verbose
e.API.AuthenticatedSupport = exch.API.AuthenticatedSupport
e.API.AuthenticatedWebsocketSupport = exch.API.AuthenticatedWebsocketSupport
if e.API.AuthenticatedSupport || e.API.AuthenticatedWebsocketSupport {
e.SetAPIKeys(exch.API.Credentials.Key,
exch.API.Credentials.Secret,
exch.API.Credentials.ClientID)
}
if exch.HTTPTimeout <= time.Duration(0) {
exch.HTTPTimeout = DefaultHTTPTimeout
} else {
err := e.SetHTTPClientTimeout(exch.HTTPTimeout)
if err != nil {
return err
}
}
if exch.CurrencyPairs == nil {
exch.CurrencyPairs = new(currency.PairsManager)
}
e.HTTPDebugging = exch.HTTPDebugging
e.SetHTTPClientUserAgent(exch.HTTPUserAgent)
e.SetCurrencyPairFormat()
err := e.SetConfigPairs()
if err != nil {
return err
}
e.SetFeatureDefaults()
err = e.SetAPIURL()
if err != nil {
return err
}
e.SetAPICredentialDefaults()
err = e.SetClientProxyAddress(exch.ProxyAddress)
if err != nil {
return err
}
e.BaseCurrencies = exch.BaseCurrencies
return nil
}
// AllowAuthenticatedRequest checks to see if the required fields have been set
// before sending an authenticated API request
func (e *Base) AllowAuthenticatedRequest() bool {
if e.SkipAuthCheck {
return true
}
// Individual package usage, allow request if API credentials are valid a
// and without needing to set AuthenticatedSupport to true
if !e.LoadedByConfig {
return e.ValidateAPICredentials()
}
// Bot usage, AuthenticatedSupport can be disabled by user if desired, so
// don't allow authenticated requests.
if !e.API.AuthenticatedSupport && !e.API.AuthenticatedWebsocketSupport {
return false
}
// Check to see if the user has enabled AuthenticatedSupport, but has
// invalid API credentials set and loaded by config
return e.ValidateAPICredentials()
}
// ValidateAPICredentials validates the exchanges API credentials
func (e *Base) ValidateAPICredentials() bool {
if e.API.CredentialsValidator.RequiresKey {
if e.API.Credentials.Key == "" ||
e.API.Credentials.Key == config.DefaultAPIKey {
log.Warnf(log.ExchangeSys,
"exchange %s requires API key but default/empty one set",
e.Name)
return false
}
}
if e.API.CredentialsValidator.RequiresSecret {
if e.API.Credentials.Secret == "" ||
e.API.Credentials.Secret == config.DefaultAPISecret {
log.Warnf(log.ExchangeSys,
"exchange %s requires API secret but default/empty one set",
e.Name)
return false
}
}
if e.API.CredentialsValidator.RequiresPEM {
if e.API.Credentials.PEMKey == "" ||
strings.Contains(e.API.Credentials.PEMKey, "JUSTADUMMY") {
log.Warnf(log.ExchangeSys,
"exchange %s requires API PEM key but default/empty one set",
e.Name)
return false
}
}
if e.API.CredentialsValidator.RequiresClientID {
if e.API.Credentials.ClientID == "" ||
e.API.Credentials.ClientID == config.DefaultAPIClientID {
log.Warnf(log.ExchangeSys,
"exchange %s requires API ClientID but default/empty one set",
e.Name)
return false
}
}
if e.API.CredentialsValidator.RequiresBase64DecodeSecret && !e.LoadedByConfig {
_, err := crypto.Base64Decode(e.API.Credentials.Secret)
if err != nil {
log.Warnf(log.ExchangeSys,
"exchange %s API secret base64 decode failed: %s",
e.Name, err)
return false
}
}
return true
}
// SetPairs sets the exchange currency pairs for either enabledPairs or
// availablePairs
func (e *Base) SetPairs(pairs currency.Pairs, assetType asset.Item, enabled bool) error {
if len(pairs) == 0 {
return fmt.Errorf("%s SetPairs error - pairs is empty", e.Name)
}
pairFmt, err := e.GetPairFormat(assetType, false)
if err != nil {
return err
}
var newPairs currency.Pairs
for x := range pairs {
newPairs = append(newPairs, pairs[x].Format(pairFmt.Delimiter,
pairFmt.Uppercase))
}
e.CurrencyPairs.StorePairs(assetType, newPairs, enabled)
e.Config.CurrencyPairs.StorePairs(assetType, newPairs, enabled)
return nil
}
// UpdatePairs updates the exchange currency pairs for either enabledPairs or
// availablePairs
func (e *Base) UpdatePairs(exchangeProducts currency.Pairs, assetType asset.Item, enabled, force bool) error {
exchangeProducts = exchangeProducts.Upper()
var products currency.Pairs
for x := range exchangeProducts {
if exchangeProducts[x].String() == "" {
continue
}
products = append(products, exchangeProducts[x])
}
var updateType string
targetPairs, err := e.CurrencyPairs.GetPairs(assetType, enabled)
if err != nil {
return err
}
if enabled {
updateType = "enabled"
} else {
updateType = "available"
}
newPairs, removedPairs := targetPairs.FindDifferences(products)
if force || len(newPairs) > 0 || len(removedPairs) > 0 {
if force {
log.Debugf(log.ExchangeSys,
"%s forced update of %s [%v] pairs.",
e.Name,
updateType,
strings.ToUpper(assetType.String()))
} else {
if len(newPairs) > 0 {
log.Debugf(log.ExchangeSys,
"%s Updating %s pairs [%v] - Added: %s.\n",
e.Name,
updateType,
strings.ToUpper(assetType.String()),
newPairs)
}
if len(removedPairs) > 0 {
log.Debugf(log.ExchangeSys,
"%s Updating %s pairs [%v] - Removed: %s.\n",
e.Name,
updateType,
strings.ToUpper(assetType.String()),
removedPairs)
}
}
e.Config.CurrencyPairs.StorePairs(assetType, products, enabled)
e.CurrencyPairs.StorePairs(assetType, products, enabled)
if !enabled {
// If available pairs are changed we will remove currency pair items
// that are still included in the enabled pairs list.
enabledPairs, err := e.CurrencyPairs.GetPairs(assetType, true)
if err == nil {
return nil
}
_, remove := enabledPairs.FindDifferences(products)
for i := range remove {
enabledPairs = enabledPairs.Remove(remove[i])
}
if len(remove) > 0 {
log.Debugf(log.ExchangeSys,
"%s Checked and updated enabled pairs [%v] - Removed: %s.\n",
e.Name,
strings.ToUpper(assetType.String()),
remove)
e.Config.CurrencyPairs.StorePairs(assetType, enabledPairs, true)
e.CurrencyPairs.StorePairs(assetType, enabledPairs, true)
}
}
}
return nil
}
// SetAPIURL sets configuration API URL for an exchange
func (e *Base) SetAPIURL() error {
if e.Config.API.Endpoints.URL == "" || e.Config.API.Endpoints.URLSecondary == "" {
return fmt.Errorf("exchange %s: SetAPIURL error. URL vals are empty", e.Name)
}
checkInsecureEndpoint := func(endpoint string) {
if strings.Contains(endpoint, "https") {
return
}
log.Warnf(log.ExchangeSys,
"%s is using HTTP instead of HTTPS [%s] for API functionality, an"+
" attacker could eavesdrop on this connection. Use at your"+
" own risk.",
e.Name, endpoint)
}
if e.Config.API.Endpoints.URL != config.APIURLNonDefaultMessage {
e.API.Endpoints.URL = e.Config.API.Endpoints.URL
checkInsecureEndpoint(e.API.Endpoints.URL)
}
if e.Config.API.Endpoints.URLSecondary != config.APIURLNonDefaultMessage {
e.API.Endpoints.URLSecondary = e.Config.API.Endpoints.URLSecondary
checkInsecureEndpoint(e.API.Endpoints.URLSecondary)
}
return nil
}
// GetAPIURL returns the set API URL
func (e *Base) GetAPIURL() string {
return e.API.Endpoints.URL
}
// GetSecondaryAPIURL returns the set Secondary API URL
func (e *Base) GetSecondaryAPIURL() string {
return e.API.Endpoints.URLSecondary
}
// GetAPIURLDefault returns exchange default URL
func (e *Base) GetAPIURLDefault() string {
return e.API.Endpoints.URLDefault
}
// GetAPIURLSecondaryDefault returns exchange default secondary URL
func (e *Base) GetAPIURLSecondaryDefault() string {
return e.API.Endpoints.URLSecondaryDefault
}
// SupportsREST returns whether or not the exchange supports
// REST
func (e *Base) SupportsREST() bool {
return e.Features.Supports.REST
}
// GetWithdrawPermissions passes through the exchange's withdraw permissions
func (e *Base) GetWithdrawPermissions() uint32 {
return e.Features.Supports.WithdrawPermissions
}
// SupportsWithdrawPermissions compares the supplied permissions with the exchange's to verify they're supported
func (e *Base) SupportsWithdrawPermissions(permissions uint32) bool {
exchangePermissions := e.GetWithdrawPermissions()
return permissions&exchangePermissions == permissions
}
// FormatWithdrawPermissions will return each of the exchange's compatible withdrawal methods in readable form
func (e *Base) FormatWithdrawPermissions() string {
var services []string
for i := 0; i < 32; i++ {
var check uint32 = 1 << uint32(i)
if e.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 (e *Base) SupportsAsset(a asset.Item) bool {
_, ok := e.CurrencyPairs.Pairs[a]
return ok
}
// PrintEnabledPairs prints the exchanges enabled asset pairs
func (e *Base) PrintEnabledPairs() {
for k, v := range e.CurrencyPairs.Pairs {
log.Infof(log.ExchangeSys, "%s Asset type %v:\n\t Enabled pairs: %v",
e.Name, strings.ToUpper(k.String()), v.Enabled)
}
}
// GetBase returns the exchange base
func (e *Base) GetBase() *Base { return e }
// CheckTransientError catches transient errors and returns nil if found, used
// for validation of API credentials
func (e *Base) CheckTransientError(err error) error {
if _, ok := err.(net.Error); ok {
log.Warnf(log.ExchangeSys,
"%s net error captured, will not disable authentication %s",
e.Name,
err)
return nil
}
return err
}
// DisableRateLimiter disables the rate limiting system for the exchange
func (e *Base) DisableRateLimiter() error {
return e.Requester.DisableRateLimiter()
}
// EnableRateLimiter enables the rate limiting system for the exchange
func (e *Base) EnableRateLimiter() error {
return e.Requester.EnableRateLimiter()
}
// StoreAssetPairFormat initialises and stores a defined asset format
func (e *Base) StoreAssetPairFormat(a asset.Item, f currency.PairStore) error {
if a.String() == "" {
return fmt.Errorf("%s cannot add to pairs manager, no asset provided",
e.Name)
}
if f.RequestFormat == nil {
return fmt.Errorf("%s cannot add to pairs manager, request pair format not provided",
e.Name)
}
if f.ConfigFormat == nil {
return fmt.Errorf("%s cannot add to pairs manager, config pair format not provided",
e.Name)
}
if e.CurrencyPairs.Pairs == nil {
e.CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore)
}
e.CurrencyPairs.Pairs[a] = &f
return nil
}
// SetGlobalPairsManager sets defined asset and pairs management system with
// with global formatting
func (e *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",
e.Name)
}
if config == nil {
return fmt.Errorf("%s cannot set pairs manager, config pair format not provided",
e.Name)
}
if len(assets) == 0 {
return fmt.Errorf("%s cannot set pairs manager, no assets provided",
e.Name)
}
e.CurrencyPairs.UseGlobalFormat = true
e.CurrencyPairs.RequestFormat = request
e.CurrencyPairs.ConfigFormat = config
if e.CurrencyPairs.Pairs != nil {
return fmt.Errorf("%s cannot set pairs manager, pairs already set",
e.Name)
}
e.CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore)
for i := range assets {
if assets[i].String() == "" {
e.CurrencyPairs.Pairs = nil
return fmt.Errorf("%s cannot set pairs manager, asset is empty string",
e.Name)
}
e.CurrencyPairs.Pairs[assets[i]] = new(currency.PairStore)
e.CurrencyPairs.Pairs[assets[i]].ConfigFormat = config
e.CurrencyPairs.Pairs[assets[i]].RequestFormat = request
}
return nil
}
// GetWebsocket returns a pointer to the exchange websocket
func (e *Base) GetWebsocket() (*stream.Websocket, error) {
if e.Websocket == nil {
return nil, common.ErrFunctionNotSupported
}
return e.Websocket, nil
}
// SupportsWebsocket returns whether or not the exchange supports
// websocket
func (e *Base) SupportsWebsocket() bool {
return e.Features.Supports.Websocket
}
// IsWebsocketEnabled returns whether or not the exchange has its
// websocket client enabled
func (e *Base) IsWebsocketEnabled() bool {
if e.Websocket == nil {
return false
}
return e.Websocket.IsEnabled()
}
// FlushWebsocketChannels refreshes websocket channel subscriptions based on
// websocket features. Used in the event of a pair/asset or subscription change.
func (e *Base) FlushWebsocketChannels() error {
if e.Websocket == nil {
return nil
}
return e.Websocket.FlushChannels()
}
// SubscribeToWebsocketChannels appends to ChannelsToSubscribe
// which lets websocket.manageSubscriptions handle subscribing
func (e *Base) SubscribeToWebsocketChannels(channels []stream.ChannelSubscription) error {
if e.Websocket == nil {
return common.ErrFunctionNotSupported
}
return e.Websocket.SubscribeToChannels(channels)
}
// UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe
// which lets websocket.manageSubscriptions handle unsubscribing
func (e *Base) UnsubscribeToWebsocketChannels(channels []stream.ChannelSubscription) error {
if e.Websocket == nil {
return common.ErrFunctionNotSupported
}
return e.Websocket.UnsubscribeChannels(channels)
}
// GetSubscriptions returns a copied list of subscriptions
func (e *Base) GetSubscriptions() ([]stream.ChannelSubscription, error) {
if e.Websocket == nil {
return nil, common.ErrFunctionNotSupported
}
return e.Websocket.GetSubscriptions(), nil
}
// AuthenticateWebsocket sends an authentication message to the websocket
func (e *Base) AuthenticateWebsocket() error {
return common.ErrFunctionNotSupported
}
// KlineIntervalEnabled returns if requested interval is enabled on exchange
func (e *Base) klineIntervalEnabled(in kline.Interval) bool {
return e.Features.Enabled.Kline.Intervals[in.Word()]
}
// FormatExchangeKlineInterval returns Interval to string
// Exchanges can override this if they require custom formatting
func (e *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 (e *Base) ValidateKline(pair currency.Pair, a asset.Item, interval kline.Interval) error {
var errorList []string
var err kline.ErrorKline
if e.CurrencyPairs.IsAssetEnabled(a) != nil {
err.Asset = a
errorList = append(errorList, "asset not enabled")
} else if !e.CurrencyPairs.Pairs[a].Enabled.Contains(pair, true) {
err.Pair = pair
errorList = append(errorList, "pair not enabled")
}
if !e.klineIntervalEnabled(interval) {
err.Interval = interval
errorList = append(errorList, "interval not supported")
}
if len(errorList) > 0 {
err.Err = errors.New(strings.Join(errorList, ","))
return &err
}
return nil
}
// AddTradesToBuffer is a helper function that will only
// add trades to the buffer if it is allowed
func (e *Base) AddTradesToBuffer(trades ...trade.Data) error {
if !e.IsSaveTradeDataEnabled() {
return nil
}
return trade.AddTradesToBuffer(e.Name, trades...)
}
// IsSaveTradeDataEnabled checks the state of
// SaveTradeData in a concurrent-friendly manner
func (e *Base) IsSaveTradeDataEnabled() bool {
e.settingsMutex.RLock()
isEnabled := e.Features.Enabled.SaveTradeData
e.settingsMutex.RUnlock()
return isEnabled
}
// SetSaveTradeDataStatus locks and sets the status of
// the config and the exchange's setting for SaveTradeData
func (e *Base) SetSaveTradeDataStatus(enabled bool) {
e.settingsMutex.Lock()
defer e.settingsMutex.Unlock()
e.Features.Enabled.SaveTradeData = enabled
e.Config.Features.Enabled.SaveTradeData = enabled
if e.Verbose {
log.Debugf(log.Trade, "Set %v 'SaveTradeData' to %v", e.Name, enabled)
}
}