Files
gocryptotrader/exchanges/exchange.go
Adrian Gallagher ac41a7cfad New features and bug fixes
- Modifications made to the request package. Planned improvements will be
sending requests on intervals, rate limiter back off support, dynamic tuning
and requests packaged into a request job group.
- Can modify each exchanges individual HTTP client (e.g timeout and
transport settings).
- Bot now uses an exchange config HTTP timeout value.
- Bot now uses a global HTTP timeout (configurable).
- Batched ticker request support for exchanges.
- Ticker and Orderbook fetching now are spanned accross multiple
go routines and regulated by a sync wait group.
- Fixes hack used to load exchanges, now uses a sync wait group.
- Ticker and Orderbook storage and fetching now uses mutex locks.
- New pair function for finding different pairs between two supplied
 pair arrays. This is used for currency pair updates for exchange which
support dynamic updating.
- Shows removal/additions of dynamic updates currencies.
2018-05-04 13:20:19 +10:00

471 lines
14 KiB
Go

package exchange
import (
"log"
"net/http"
"sync"
"time"
"github.com/thrasher-/gocryptotrader/common"
"github.com/thrasher-/gocryptotrader/config"
"github.com/thrasher-/gocryptotrader/currency/pair"
"github.com/thrasher-/gocryptotrader/exchanges/nonce"
"github.com/thrasher-/gocryptotrader/exchanges/orderbook"
"github.com/thrasher-/gocryptotrader/exchanges/request"
"github.com/thrasher-/gocryptotrader/exchanges/ticker"
)
const (
warningBase64DecryptSecretKeyFailed = "WARNING -- Exchange %s unable to base64 decode secret key.. Disabling Authenticated API support."
// WarningAuthenticatedRequestWithoutCredentialsSet error message for authenticated request without credentials set
WarningAuthenticatedRequestWithoutCredentialsSet = "WARNING -- Exchange %s authenticated HTTP request called but not supported due to unset/default API keys."
// ErrExchangeNotFound is a constant for an error message
ErrExchangeNotFound = "Exchange not found in dataset."
// DefaultHTTPTimeout is the default HTTP/HTTPS Timeout for exchange requests
DefaultHTTPTimeout = time.Second * 15
)
// AccountInfo is a Generic type to hold each exchange's holdings in
// all enabled currencies
type AccountInfo struct {
ExchangeName string
Currencies []AccountCurrencyInfo
}
// AccountCurrencyInfo is a sub type to store currency name and value
type AccountCurrencyInfo struct {
CurrencyName string
TotalValue float64
Hold float64
}
// TradeHistory holds exchange history data
type TradeHistory struct {
Timestamp int64
TID int64
Price float64
Amount float64
Exchange string
Type string
}
// Base stores the individual exchange information
type Base struct {
Name string
Enabled bool
Verbose bool
Websocket bool
RESTPollingDelay time.Duration
AuthenticatedAPISupport bool
APISecret, APIKey, ClientID string
Nonce nonce.Nonce
TakerFee, MakerFee, Fee float64
BaseCurrencies []string
AvailablePairs []string
EnabledPairs []string
AssetTypes []string
PairsLastUpdated int64
SupportsAutoPairUpdating bool
SupportsRESTTickerBatching bool
HTTPTimeout time.Duration
WebsocketURL string
APIUrl string
RequestCurrencyPairFormat config.CurrencyPairFormatConfig
ConfigCurrencyPairFormat config.CurrencyPairFormatConfig
*request.Requester
}
// IBotExchange enforces standard functions for all exchanges supported in
// GoCryptoTrader
type IBotExchange interface {
Setup(exch config.ExchangeConfig)
Start(wg *sync.WaitGroup)
SetDefaults()
GetName() string
IsEnabled() bool
SetEnabled(bool)
GetTickerPrice(currency pair.CurrencyPair, assetType string) (ticker.Price, error)
UpdateTicker(currency pair.CurrencyPair, assetType string) (ticker.Price, error)
GetOrderbookEx(currency pair.CurrencyPair, assetType string) (orderbook.Base, error)
UpdateOrderbook(currency pair.CurrencyPair, assetType string) (orderbook.Base, error)
GetEnabledCurrencies() []pair.CurrencyPair
GetAvailableCurrencies() []pair.CurrencyPair
GetExchangeAccountInfo() (AccountInfo, error)
GetAuthenticatedAPISupport() bool
SetCurrencies(pairs []pair.CurrencyPair, enabledPairs bool) error
GetExchangeHistory(pair.CurrencyPair, string) ([]TradeHistory, error)
SupportsAutoPairUpdates() bool
GetLastPairsUpdateTime() int64
SupportsRESTTickerBatchUpdates() bool
}
// SupportsRESTTickerBatchUpdates returns whether or not the
// exhange supports REST batch ticker fetching
func (e *Base) SupportsRESTTickerBatchUpdates() bool {
return e.SupportsRESTTickerBatching
}
// SetHTTPClientTimeout sets the timeout value for the exchanges
// HTTP Client
func (e *Base) SetHTTPClientTimeout(t time.Duration) {
if e.Requester == nil {
e.Requester = request.New(e.Name, request.NewRateLimit(time.Second, 0), request.NewRateLimit(time.Second, 0), new(http.Client))
}
e.Requester.HTTPClient.Timeout = t
}
// SetHTTPClient sets exchanges HTTP client
func (e *Base) SetHTTPClient(h *http.Client) {
if e.Requester == nil {
e.Requester = request.New(e.Name, request.NewRateLimit(time.Second, 0), request.NewRateLimit(time.Second, 0), new(http.Client))
}
e.Requester.HTTPClient = h
}
// GetHTTPClient gets the exchanges HTTP client
func (e *Base) GetHTTPClient() *http.Client {
if e.Requester == nil {
e.Requester = request.New(e.Name, request.NewRateLimit(time.Second, 0), request.NewRateLimit(time.Second, 0), new(http.Client))
}
return e.Requester.HTTPClient
}
// SetAutoPairDefaults sets the default values for whether or not the exchange
// supports auto pair updating or not
func (e *Base) SetAutoPairDefaults() error {
cfg := config.GetConfig()
exch, err := cfg.GetExchangeConfig(e.Name)
if err != nil {
return err
}
update := false
if e.SupportsAutoPairUpdating {
if !exch.SupportsAutoPairUpdates {
exch.SupportsAutoPairUpdates = true
update = true
}
} else {
if exch.PairsLastUpdated == 0 {
exch.PairsLastUpdated = time.Now().Unix()
e.PairsLastUpdated = exch.PairsLastUpdated
update = true
}
}
if update {
return cfg.UpdateExchangeConfig(exch)
}
return nil
}
// SupportsAutoPairUpdates returns whether or not the exchange supports
// auto currency pair updating
func (e *Base) SupportsAutoPairUpdates() bool {
return e.SupportsAutoPairUpdating
}
// GetLastPairsUpdateTime returns the unix timestamp of when the exchanges
// currency pairs were last updated
func (e *Base) GetLastPairsUpdateTime() int64 {
return e.PairsLastUpdated
}
// SetAssetTypes checks the exchange asset types (whether it supports SPOT,
// Binary or Futures) and sets it to a default setting if it doesn't exist
func (e *Base) SetAssetTypes() error {
cfg := config.GetConfig()
exch, err := cfg.GetExchangeConfig(e.Name)
if err != nil {
return err
}
update := false
if exch.AssetTypes == "" {
exch.AssetTypes = common.JoinStrings(e.AssetTypes, ",")
update = true
} else {
e.AssetTypes = common.SplitStrings(exch.AssetTypes, ",")
}
if update {
return cfg.UpdateExchangeConfig(exch)
}
return nil
}
// GetExchangeAssetTypes returns the asset types the exchange supports (SPOT,
// binary, futures)
func GetExchangeAssetTypes(exchName string) ([]string, error) {
cfg := config.GetConfig()
exch, err := cfg.GetExchangeConfig(exchName)
if err != nil {
return nil, err
}
return common.SplitStrings(exch.AssetTypes, ","), nil
}
// CompareCurrencyPairFormats checks and returns whether or not the two supplied
// config currency pairs match
func CompareCurrencyPairFormats(pair1 config.CurrencyPairFormatConfig, pair2 *config.CurrencyPairFormatConfig) bool {
if pair1.Delimiter != pair2.Delimiter ||
pair1.Uppercase != pair2.Uppercase ||
pair1.Separator != pair2.Separator ||
pair1.Index != pair2.Index {
return false
}
return true
}
// SetCurrencyPairFormat checks the exchange request and config currency pair
// formats and sets it to a default setting if it doesn't exist
func (e *Base) SetCurrencyPairFormat() error {
cfg := config.GetConfig()
exch, err := cfg.GetExchangeConfig(e.Name)
if err != nil {
return err
}
update := false
if exch.RequestCurrencyPairFormat == nil {
exch.RequestCurrencyPairFormat = &config.CurrencyPairFormatConfig{
Delimiter: e.RequestCurrencyPairFormat.Delimiter,
Uppercase: e.RequestCurrencyPairFormat.Uppercase,
Separator: e.RequestCurrencyPairFormat.Separator,
Index: e.RequestCurrencyPairFormat.Index,
}
update = true
} else {
if CompareCurrencyPairFormats(e.RequestCurrencyPairFormat,
exch.RequestCurrencyPairFormat) {
e.RequestCurrencyPairFormat = *exch.RequestCurrencyPairFormat
} else {
*exch.RequestCurrencyPairFormat = e.ConfigCurrencyPairFormat
update = true
}
}
if exch.ConfigCurrencyPairFormat == nil {
exch.ConfigCurrencyPairFormat = &config.CurrencyPairFormatConfig{
Delimiter: e.ConfigCurrencyPairFormat.Delimiter,
Uppercase: e.ConfigCurrencyPairFormat.Uppercase,
Separator: e.ConfigCurrencyPairFormat.Separator,
Index: e.ConfigCurrencyPairFormat.Index,
}
update = true
} else {
if CompareCurrencyPairFormats(e.ConfigCurrencyPairFormat,
exch.ConfigCurrencyPairFormat) {
e.ConfigCurrencyPairFormat = *exch.ConfigCurrencyPairFormat
} else {
*exch.ConfigCurrencyPairFormat = e.ConfigCurrencyPairFormat
update = true
}
}
if update {
return cfg.UpdateExchangeConfig(exch)
}
return nil
}
// GetAuthenticatedAPISupport returns whether the exchange supports
// authenticated API requests
func (e *Base) GetAuthenticatedAPISupport() bool {
return e.AuthenticatedAPISupport
}
// GetName is a method that returns the name of the exchange base
func (e *Base) GetName() string {
return e.Name
}
// GetEnabledCurrencies is a method that returns the enabled currency pairs of
// the exchange base
func (e *Base) GetEnabledCurrencies() []pair.CurrencyPair {
return pair.FormatPairs(e.EnabledPairs,
e.ConfigCurrencyPairFormat.Delimiter,
e.ConfigCurrencyPairFormat.Index)
}
// GetAvailableCurrencies is a method that returns the available currency pairs
// of the exchange base
func (e *Base) GetAvailableCurrencies() []pair.CurrencyPair {
return pair.FormatPairs(e.AvailablePairs,
e.ConfigCurrencyPairFormat.Delimiter,
e.ConfigCurrencyPairFormat.Index)
}
// SupportsCurrency returns true or not whether a currency pair exists in the
// exchange available currencies or not
func (e *Base) SupportsCurrency(p pair.CurrencyPair, enabledPairs bool) bool {
if enabledPairs {
return pair.Contains(e.GetEnabledCurrencies(), p, false)
}
return pair.Contains(e.GetAvailableCurrencies(), p, false)
}
// GetExchangeFormatCurrencySeperator returns whether or not a specific
// exchange contains a separator used for API requests
func GetExchangeFormatCurrencySeperator(exchName string) bool {
cfg := config.GetConfig()
exch, err := cfg.GetExchangeConfig(exchName)
if err != nil {
return false
}
if exch.RequestCurrencyPairFormat.Separator != "" {
return true
}
return false
}
// GetAndFormatExchangeCurrencies returns a pair.CurrencyItem string containing
// the exchanges formatted currency pairs
func GetAndFormatExchangeCurrencies(exchName string, pairs []pair.CurrencyPair) (pair.CurrencyItem, error) {
var currencyItems pair.CurrencyItem
cfg := config.GetConfig()
exch, err := cfg.GetExchangeConfig(exchName)
if err != nil {
return currencyItems, err
}
for x := range pairs {
currencyItems += FormatExchangeCurrency(exchName, pairs[x])
if x == len(pairs)-1 {
continue
}
currencyItems += pair.CurrencyItem(exch.RequestCurrencyPairFormat.Separator)
}
return currencyItems, nil
}
// FormatExchangeCurrency is a method that formats and returns a currency pair
// based on the user currency display preferences
func FormatExchangeCurrency(exchName string, p pair.CurrencyPair) pair.CurrencyItem {
cfg := config.GetConfig()
exch, _ := cfg.GetExchangeConfig(exchName)
return p.Display(exch.RequestCurrencyPairFormat.Delimiter,
exch.RequestCurrencyPairFormat.Uppercase)
}
// FormatCurrency is a method that formats and returns a currency pair
// based on the user currency display preferences
func FormatCurrency(p pair.CurrencyPair) pair.CurrencyItem {
cfg := config.GetConfig()
return p.Display(cfg.CurrencyPairFormat.Delimiter,
cfg.CurrencyPairFormat.Uppercase)
}
// 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, b64Decode bool) {
if !e.AuthenticatedAPISupport {
return
}
e.APIKey = APIKey
e.ClientID = ClientID
if b64Decode {
result, err := common.Base64Decode(APISecret)
if err != nil {
e.AuthenticatedAPISupport = false
log.Printf(warningBase64DecryptSecretKeyFailed, e.Name)
}
e.APISecret = string(result)
} else {
e.APISecret = APISecret
}
}
// SetCurrencies sets the exchange currency pairs for either enabledPairs or
// availablePairs
func (e *Base) SetCurrencies(pairs []pair.CurrencyPair, enabledPairs bool) error {
cfg := config.GetConfig()
exchCfg, err := cfg.GetExchangeConfig(e.Name)
if err != nil {
return err
}
var pairsStr []string
for x := range pairs {
pairsStr = append(pairsStr, pairs[x].Display(exchCfg.ConfigCurrencyPairFormat.Delimiter,
exchCfg.ConfigCurrencyPairFormat.Uppercase).String())
}
if enabledPairs {
exchCfg.EnabledPairs = common.JoinStrings(pairsStr, ",")
e.EnabledPairs = pairsStr
} else {
exchCfg.AvailablePairs = common.JoinStrings(pairsStr, ",")
e.AvailablePairs = pairsStr
}
return cfg.UpdateExchangeConfig(exchCfg)
}
// UpdateCurrencies updates the exchange currency pairs for either enabledPairs or
// availablePairs
func (e *Base) UpdateCurrencies(exchangeProducts []string, enabled, force bool) error {
exchangeProducts = common.SplitStrings(common.StringToUpper(common.JoinStrings(exchangeProducts, ",")), ",")
var products []string
for x := range exchangeProducts {
if exchangeProducts[x] == "" {
continue
}
products = append(products, exchangeProducts[x])
}
var newPairs, removedPairs []string
if enabled {
newPairs, removedPairs = pair.FindPairDifferences(e.EnabledPairs, products)
} else {
newPairs, removedPairs = pair.FindPairDifferences(e.AvailablePairs, products)
}
if force || len(newPairs) > 0 || len(removedPairs) > 0 {
cfg := config.GetConfig()
exch, err := cfg.GetExchangeConfig(e.Name)
if err != nil {
return err
}
if force {
log.Printf("%s forced update of enabled pairs.", e.Name)
} else {
if len(newPairs) > 0 {
log.Printf("%s Updating pairs - New: %s.\n", e.Name, newPairs)
}
if len(removedPairs) > 0 {
log.Printf("%s Updating pairs - Removed: %s.\n", e.Name, removedPairs)
}
}
if enabled {
exch.EnabledPairs = common.JoinStrings(products, ",")
e.EnabledPairs = products
} else {
exch.AvailablePairs = common.JoinStrings(products, ",")
e.AvailablePairs = products
}
return cfg.UpdateExchangeConfig(exch)
}
return nil
}