From f843b7d27726c13718a6370dbd6d46d8570aa1b2 Mon Sep 17 00:00:00 2001 From: Ryan O'Hara-Reid Date: Fri, 16 Sep 2022 08:59:27 +1000 Subject: [PATCH] exchange: upgrade UpdatePair method (#991) * exchange: upgrade update pair * exchanges: Add enabled string matching and format handling if discrepency is found. * linter: fixes * bithumb: fix tests * BTSE: api change fix ordering * huobi: fix tests * gloriousnits: stage 1 * gloriousnits: stage 2 * currency: more nits * bitmex: add spot and process pairs before currency package call. * bitmex: finished correct orderbook matching and other implementations * linter: fix issue * currency: Fix linter * currency: segregate and protect pair store, update tests * currency/manager: clean code, rm log output * currency: Add store method and make sure formatting stays nil if not stored. * gct: check errors * engine/websocketroutineman: fix tests * bybit: fix duplication bug * huobi: fix test * btse: fix tests? * ob/buffer: fix tests * Update currency/manager.go Co-authored-by: Scott * glorious: nits * glorious: nits strikes again. * exchange: add bypassConfigFormatUpgrades to stop formatting * GLORIOUS LINTER * Update exchanges/bithumb/bithumb_wrapper.go Co-authored-by: Scott * glorious: nits * exchange: fix pair upgrade issue when duplications are in both avail and enabled pairs * linter: fix shadow dec * config: fix test * Update currency/pair_test.go Co-authored-by: Scott Co-authored-by: Ryan O'Hara-Reid Co-authored-by: Scott --- backtester/engine/setup.go | 2 +- .../ftxcashandcarry/ftxcashandcarry.go | 8 +- backtester/report/chart.go | 5 +- cmd/exchange_template/exchange_template.go | 1 + cmd/exchange_wrapper_issues/main.go | 28 +- config/config.go | 83 ++-- config/config_test.go | 45 +- currency/currency_types.go | 6 +- currency/manager.go | 244 ++++++++--- currency/manager_test.go | 188 +++++++-- currency/pair.go | 6 +- currency/pair_methods.go | 9 +- currency/pair_test.go | 96 +++-- currency/pair_types.go | 8 + currency/pairs.go | 167 ++++++-- currency/pairs_test.go | 223 +++++++++- engine/engine.go | 5 +- engine/helpers_test.go | 2 +- engine/rpcserver.go | 72 +++- engine/rpcserver_test.go | 12 +- engine/sync_manager.go | 5 +- engine/sync_manager_test.go | 10 +- engine/sync_manager_types.go | 3 +- engine/websocketroutine_manager.go | 14 +- engine/websocketroutine_manager_test.go | 6 +- exchanges/binance/binance_wrapper.go | 7 +- exchanges/bitfinex/bitfinex_wrapper.go | 19 +- exchanges/bithumb/bithumb_test.go | 5 + exchanges/bithumb/bithumb_websocket.go | 7 +- exchanges/bithumb/bithumb_websocket_test.go | 1 + exchanges/bithumb/bithumb_wrapper.go | 4 +- exchanges/bitmex/bitmex.go | 8 + exchanges/bitmex/bitmex_types.go | 203 +++++---- exchanges/bitmex/bitmex_websocket.go | 42 +- exchanges/bitmex/bitmex_wrapper.go | 152 ++++--- exchanges/bitstamp/bitstamp_wrapper.go | 2 +- exchanges/btse/btse_wrapper.go | 3 +- exchanges/bybit/bybit_wrapper.go | 12 +- exchanges/bybit/bybit_ws_futures.go | 4 - exchanges/bybit/bybit_ws_ufutures.go | 7 +- exchanges/exchange.go | 342 ++++++++++----- exchanges/exchange_test.go | 394 +++++++++++++++--- exchanges/ftx/ftx_test.go | 12 +- exchanges/ftx/ftx_wrapper.go | 2 +- exchanges/hitbtc/hitbtc_test.go | 5 +- exchanges/huobi/huobi_futures.go | 2 +- exchanges/huobi/huobi_test.go | 7 +- exchanges/huobi/huobi_wrapper.go | 2 + exchanges/itbit/itbit_wrapper.go | 8 +- .../localbitcoins/localbitcoins_wrapper.go | 4 +- exchanges/okcoin/okcoin_test.go | 4 +- exchanges/okex/okex_test.go | 4 +- exchanges/okex/okex_wrapper.go | 1 + exchanges/stream/buffer/buffer_test.go | 4 +- exchanges/zb/zb_wrapper.go | 2 +- 55 files changed, 1832 insertions(+), 685 deletions(-) diff --git a/backtester/engine/setup.go b/backtester/engine/setup.go index 95c43b9a..dcb891d1 100644 --- a/backtester/engine/setup.go +++ b/backtester/engine/setup.go @@ -201,7 +201,7 @@ func NewFromConfig(cfg *config.Config, templatePath, output string, verbose bool if err != nil { return nil, fmt.Errorf("could not get pair format %v, %w", curr, err) } - curr = curr.Format(requestFormat.Delimiter, requestFormat.Uppercase) + curr = curr.Format(requestFormat) var avail, enabled currency.Pairs avail, err = exch.GetAvailablePairs(a) if err != nil { diff --git a/backtester/eventhandlers/strategies/ftxcashandcarry/ftxcashandcarry.go b/backtester/eventhandlers/strategies/ftxcashandcarry/ftxcashandcarry.go index a21af9ee..bdb7cec0 100644 --- a/backtester/eventhandlers/strategies/ftxcashandcarry/ftxcashandcarry.go +++ b/backtester/eventhandlers/strategies/ftxcashandcarry/ftxcashandcarry.go @@ -172,14 +172,14 @@ func sortSignals(d []data.Handler) (map[currency.Pair]cashCarrySignals, error) { a := l.GetAssetType() switch { case a == asset.Spot: - entry := response[l.Pair().Format("", false)] + entry := response[l.Pair().Format(currency.EMPTYFORMAT)] entry.spotSignal = d[i] - response[l.Pair().Format("", false)] = entry + response[l.Pair().Format(currency.EMPTYFORMAT)] = entry case a.IsFutures(): u := l.GetUnderlyingPair() - entry := response[u.Format("", false)] + entry := response[u.Format(currency.EMPTYFORMAT)] entry.futureSignal = d[i] - response[u.Format("", false)] = entry + response[u.Format(currency.EMPTYFORMAT)] = entry default: return nil, errFuturesOnly } diff --git a/backtester/report/chart.go b/backtester/report/chart.go index 49d6d94f..b9c35ca9 100644 --- a/backtester/report/chart.go +++ b/backtester/report/chart.go @@ -137,11 +137,12 @@ func createFuturesSpotDiffChart(items map[string]map[asset.Item]map[currency.Pai AxisType: "linear", } + upperFormat := currency.PairFormat{Uppercase: true} for _, assetMap := range items { for item, pairMap := range assetMap { for pair, result := range pairMap { if item.IsFutures() { - p := result.UnderlyingPair.Format("", true) + p := result.UnderlyingPair.Format(upperFormat) diff, ok := currs[p] if !ok { diff = linkCurrencyDiff{} @@ -151,7 +152,7 @@ func createFuturesSpotDiffChart(items map[string]map[asset.Item]map[currency.Pai diff.FuturesEvents = result.Events currs[p] = diff } else { - p := pair.Format("", true) + p := pair.Format(upperFormat) diff, ok := currs[p] if !ok { diff = linkCurrencyDiff{} diff --git a/cmd/exchange_template/exchange_template.go b/cmd/exchange_template/exchange_template.go index da806d2b..2c1750d2 100644 --- a/cmd/exchange_template/exchange_template.go +++ b/cmd/exchange_template/exchange_template.go @@ -160,6 +160,7 @@ func makeExchange(exchangeDirectory string, configTestFile *config.Config, exch }, ConfigFormat: ¤cy.PairFormat{ Uppercase: true, + Delimiter: currency.DashDelimiter, }, } diff --git a/cmd/exchange_wrapper_issues/main.go b/cmd/exchange_wrapper_issues/main.go index 9fb9b5d2..eb5064d1 100644 --- a/cmd/exchange_wrapper_issues/main.go +++ b/cmd/exchange_wrapper_issues/main.go @@ -299,31 +299,31 @@ func testWrappers(e exchange.IBotExchange, base *exchange.Base, config *Config) for i := range assetTypes { var msg string log.Printf("%v %v", base.GetName(), assetTypes[i]) - if _, ok := base.Config.CurrencyPairs.Pairs[assetTypes[i]]; !ok { + storedPairs, ok := base.Config.CurrencyPairs.Pairs[assetTypes[i]] + if !ok { continue } var p currency.Pair + var err error switch { case currencyPairOverride != "": - var err error p, err = currency.NewPairFromString(currencyPairOverride) - if err != nil { - log.Printf("%v Encountered error: '%v'", base.GetName(), err) - continue + case len(storedPairs.Enabled) == 0: + if len(storedPairs.Available) == 0 { + err = fmt.Errorf("%v has no enabled or available currencies. Skipping", base.GetName()) + break } - case len(base.Config.CurrencyPairs.Pairs[assetTypes[i]].Enabled) == 0: - if len(base.Config.CurrencyPairs.Pairs[assetTypes[i]].Available) == 0 { - log.Printf("%v has no enabled or available currencies. Skipping", - base.GetName()) - continue - } - p = base.Config.CurrencyPairs.Pairs[assetTypes[i]].Available.GetRandomPair() + p, err = storedPairs.Available.GetRandomPair() default: - p = base.Config.CurrencyPairs.Pairs[assetTypes[i]].Enabled.GetRandomPair() + p, err = storedPairs.Enabled.GetRandomPair() + } + + if err != nil { + log.Printf("%v Encountered error: '%v'", base.GetName(), err) + continue } - var err error p, err = disruptFormatting(p) if err != nil { log.Println("failed to disrupt currency pair formatting:", err) diff --git a/config/config.go b/config/config.go index 8025f137..e0b564b5 100644 --- a/config/config.go +++ b/config/config.go @@ -28,8 +28,11 @@ import ( "github.com/thrasher-corp/gocryptotrader/portfolio/banking" ) -// errExchangeConfigIsNil defines an error when the config is nil -var errExchangeConfigIsNil = errors.New("exchange config is nil") +var ( + // errExchangeConfigIsNil defines an error when the config is nil + errExchangeConfigIsNil = errors.New("exchange config is nil") + errPairsManagerIsNil = errors.New("currency pairs manager is nil") +) // GetCurrencyConfig returns currency configurations func (c *Config) GetCurrencyConfig() currency.Config { @@ -346,7 +349,7 @@ func (c *Config) GetExchangeAssetTypes(exchName string) (asset.Items, error) { } if exchCfg.CurrencyPairs == nil { - return nil, fmt.Errorf("exchange %s currency pairs is nil", exchName) + return nil, fmt.Errorf("%s %w", exchName, errPairsManagerIsNil) } return exchCfg.CurrencyPairs.GetAssetTypes(false), nil @@ -360,7 +363,7 @@ func (c *Config) SupportsExchangeAssetType(exchName string, assetType asset.Item } if exchCfg.CurrencyPairs == nil { - return fmt.Errorf("exchange %s currency pairs is nil", exchName) + return fmt.Errorf("%s %w", exchName, errPairsManagerIsNil) } if !assetType.IsValid() { @@ -389,8 +392,7 @@ func (c *Config) SetPairs(exchName string, assetType asset.Item, enabled bool, p return err } - exchCfg.CurrencyPairs.StorePairs(assetType, pairs, enabled) - return nil + return exchCfg.CurrencyPairs.StorePairs(assetType, pairs, enabled) } // GetCurrencyPairConfig returns currency pair config for the desired exchange and asset type @@ -507,10 +509,13 @@ func (c *Config) CheckPairConsistency(exchName string) error { return err } - err = c.SetPairs(exchName, - assetTypes[x], - true, - currency.Pairs{availPairs.GetRandomPair()}) + var rPair currency.Pair + rPair, err = availPairs.GetRandomPair() + if err != nil { + return err + } + + err = c.SetPairs(exchName, assetTypes[x], true, currency.Pairs{rPair}) if err != nil { return err } @@ -565,10 +570,13 @@ func (c *Config) CheckPairConsistency(exchName string) error { continue } - err = c.SetPairs(exchName, - assetTypes[x], - true, - currency.Pairs{availPairs.GetRandomPair()}) + var rPair currency.Pair + rPair, err = availPairs.GetRandomPair() + if err != nil { + return err + } + + err = c.SetPairs(exchName, assetTypes[x], true, currency.Pairs{rPair}) if err != nil { return err } @@ -583,8 +591,16 @@ func (c *Config) CheckPairConsistency(exchName string) error { return err } - newPair := avail.GetRandomPair() - err = c.SetPairs(exchName, assetTypes[0], true, currency.Pairs{newPair}) + if len(avail) == 0 { + return nil + } + + rPair, err := avail.GetRandomPair() + if err != nil { + return err + } + + err = c.SetPairs(exchName, assetTypes[0], true, currency.Pairs{rPair}) if err != nil { return err } @@ -592,7 +608,7 @@ func (c *Config) CheckPairConsistency(exchName string) error { "Exchange %s: [%v] No enabled pairs found in available pairs list, randomly added %v pair.\n", exchName, assetTypes[0], - newPair) + rPair) } return nil } @@ -611,12 +627,12 @@ func (c *Config) SupportsPair(exchName string, p currency.Pair, assetType asset. func (c *Config) GetPairFormat(exchName string, assetType asset.Item) (currency.PairFormat, error) { exchCfg, err := c.GetExchangeConfig(exchName) if err != nil { - return currency.PairFormat{}, err + return currency.EMPTYFORMAT, err } err = c.SupportsExchangeAssetType(exchName, assetType) if err != nil { - return currency.PairFormat{}, err + return currency.EMPTYFORMAT, err } if exchCfg.CurrencyPairs.UseGlobalFormat { @@ -625,18 +641,18 @@ func (c *Config) GetPairFormat(exchName string, assetType asset.Item) (currency. p, err := exchCfg.CurrencyPairs.Get(assetType) if err != nil { - return currency.PairFormat{}, err + return currency.EMPTYFORMAT, err } if p == nil { - return currency.PairFormat{}, + return currency.EMPTYFORMAT, fmt.Errorf("exchange %s pair store for asset type %s is nil", exchName, assetType) } if p.ConfigFormat == nil { - return currency.PairFormat{}, + return currency.EMPTYFORMAT, fmt.Errorf("exchange %s pair config format for asset type %s is nil", exchName, assetType) @@ -666,8 +682,7 @@ func (c *Config) GetAvailablePairs(exchName string, assetType asset.Item) (curre return nil, nil } - return pairs.Format(pairFormat.Delimiter, pairFormat.Index, - pairFormat.Uppercase), nil + return pairs.Format(pairFormat), nil } // GetEnabledPairs returns a list of currency pairs for a specifc exchange @@ -691,10 +706,7 @@ func (c *Config) GetEnabledPairs(exchName string, assetType asset.Item) (currenc return nil, nil } - return pairs.Format(pairFormat.Delimiter, - pairFormat.Index, - pairFormat.Uppercase), - nil + return pairs.Format(pairFormat), nil } // GetEnabledExchanges returns a list of enabled exchanges @@ -853,13 +865,14 @@ func (c *Config) CheckExchangeConfigValues() error { } c.Exchanges[i].CurrencyPairs.UseGlobalFormat = true - c.Exchanges[i].CurrencyPairs.Store(asset.Spot, - currency.PairStore{ - AssetEnabled: convert.BoolPtr(true), - Available: availPairs, - Enabled: enabledPairs, - }, - ) + err := c.Exchanges[i].CurrencyPairs.Store(asset.Spot, ¤cy.PairStore{ + AssetEnabled: convert.BoolPtr(true), + Available: availPairs, + Enabled: enabledPairs, + }) + if err != nil { + return err + } // flush old values c.Exchanges[i].PairsLastUpdated = nil diff --git a/config/config_test.go b/config/config_test.go index 4b5c1b8c..cbabca10 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -675,12 +675,12 @@ func TestCheckPairConfigFormats(t *testing.T) { c.Exchanges[0].CurrencyPairs = ¤cy.PairsManager{ Pairs: map[asset.Item]*currency.PairStore{ asset.Spot: { - RequestFormat: ¤cy.PairFormat{}, - ConfigFormat: ¤cy.PairFormat{}, + RequestFormat: ¤cy.EMPTYFORMAT, + ConfigFormat: ¤cy.EMPTYFORMAT, }, asset.Futures: { - RequestFormat: ¤cy.PairFormat{}, - ConfigFormat: ¤cy.PairFormat{}, + RequestFormat: ¤cy.EMPTYFORMAT, + ConfigFormat: ¤cy.EMPTYFORMAT, }, }, } @@ -748,8 +748,9 @@ func TestCheckPairConsistency(t *testing.T) { t.Parallel() var c Config - if err := c.CheckPairConsistency("asdf"); err == nil { - t.Error("non-existent exchange should return an error") + err := c.CheckPairConsistency("asdf") + if !errors.Is(err, ErrExchangeNotFound) { + t.Fatalf("received: '%v' buy expected: '%v'", err, ErrExchangeNotFound) } c.Exchanges = append(c.Exchanges, @@ -759,8 +760,9 @@ func TestCheckPairConsistency(t *testing.T) { ) // Test nil pair store - if err := c.CheckPairConsistency(testFakeExchangeName); err == nil { - t.Error("nil pair store should return an error") + err = c.CheckPairConsistency(testFakeExchangeName) + if !errors.Is(err, errPairsManagerIsNil) { + t.Fatalf("received: '%v' buy expected: '%v'", err, errPairsManagerIsNil) } enabled, err := currency.NewPairDelimiter("BTC_USD", "_") @@ -788,8 +790,8 @@ func TestCheckPairConsistency(t *testing.T) { // Test for nil avail pairs err = c.CheckPairConsistency(testFakeExchangeName) - if err != nil { - t.Error(err) + if !errors.Is(err, nil) { + t.Fatalf("received: '%v' buy expected: '%v'", err, nil) } p1, err := currency.NewPairDelimiter("LTC_USD", "_") @@ -802,17 +804,19 @@ func TestCheckPairConsistency(t *testing.T) { p1, } - // LTC_USD is only found in the available pairs list and should therefor + // LTC_USD is only found in the available pairs list and should therefore // be added to the enabled pairs list due to the atLestOneEnabled code err = c.CheckPairConsistency(testFakeExchangeName) if err != nil { t.Fatal(err) } - for _, item := range c.Exchanges[0].CurrencyPairs.Pairs[asset.Spot].Enabled { - if !item.Equal(p1) { - t.Fatal("LTC_USD should be contained in the enabled pairs list") - } + if len(c.Exchanges[0].CurrencyPairs.Pairs[asset.Spot].Enabled) != 1 { + t.Fatal("there should be atleast one pair located in this list") + } + + if !c.Exchanges[0].CurrencyPairs.Pairs[asset.Spot].Enabled[0].Equal(p1) { + t.Fatal("LTC_USD should be contained in the enabled pairs list") } p2, err := currency.NewPairDelimiter("BTC_USD", "_") @@ -822,8 +826,7 @@ func TestCheckPairConsistency(t *testing.T) { // Add the BTC_USD pair and see result c.Exchanges[0].CurrencyPairs.Pairs[asset.Spot].Available = currency.Pairs{ - p1, - p2, + p1, p2, } if err := c.CheckPairConsistency(testFakeExchangeName); err != nil { @@ -833,7 +836,7 @@ func TestCheckPairConsistency(t *testing.T) { // Test that an empty enabled pair is populated with an available pair c.Exchanges[0].CurrencyPairs.Pairs[asset.Spot].Enabled = nil if err := c.CheckPairConsistency(testFakeExchangeName); err != nil { - t.Error("unexpected result") + t.Error("unexpected result", err) } if len(c.Exchanges[0].CurrencyPairs.Pairs[asset.Spot].Enabled) != 1 { @@ -900,7 +903,7 @@ func TestCheckPairConsistency(t *testing.T) { func TestSupportsPair(t *testing.T) { t.Parallel() - fmt := ¤cy.PairFormat{} + fmt := ¤cy.EMPTYFORMAT cfg := &Config{ Exchanges: []Exchange{ { @@ -1858,8 +1861,8 @@ func TestCheckConfig(t *testing.T) { AssetEnabled: convert.BoolPtr(true), Available: currency.Pairs{cp1, cp2}, Enabled: currency.Pairs{cp1}, - ConfigFormat: ¤cy.PairFormat{}, - RequestFormat: ¤cy.PairFormat{}, + ConfigFormat: ¤cy.EMPTYFORMAT, + RequestFormat: ¤cy.EMPTYFORMAT, }, }, }, diff --git a/currency/currency_types.go b/currency/currency_types.go index ced6f5bb..f69e75c9 100644 --- a/currency/currency_types.go +++ b/currency/currency_types.go @@ -83,7 +83,9 @@ const ( ) // delimiters is a delimiter list -var delimiters = []string{UnderscoreDelimiter, +var delimiters = []string{ DashDelimiter, + UnderscoreDelimiter, ForwardSlashDelimiter, - ColonDelimiter} + ColonDelimiter, +} diff --git a/currency/manager.go b/currency/manager.go index d56f3ca6..ca563ff4 100644 --- a/currency/manager.go +++ b/currency/manager.go @@ -23,13 +23,20 @@ var ( // ErrAssetIsNil is an error when the asset has not been populated by the // configuration ErrAssetIsNil = errors.New("asset is nil") + // ErrPairNotContainedInAvailablePairs defines an error when a pair is not + // contained in the available pairs list and is not supported by the + // exchange for that asset type. + ErrPairNotContainedInAvailablePairs = errors.New("pair not contained in available pairs") + + errPairStoreIsNil = errors.New("pair store is nil") + errPairFormatIsNil = errors.New("pair format is nil") ) // GetAssetTypes returns a list of stored asset types func (p *PairsManager) GetAssetTypes(enabled bool) asset.Items { p.m.RLock() defer p.m.RUnlock() - var assetTypes asset.Items + assetTypes := make(asset.Items, 0, len(p.Pairs)) for k, ps := range p.Pairs { if enabled && (ps.AssetEnabled == nil || !*ps.AssetEnabled) { continue @@ -41,6 +48,10 @@ func (p *PairsManager) GetAssetTypes(enabled bool) asset.Items { // Get gets the currency pair config based on the asset type func (p *PairsManager) Get(a asset.Item) (*PairStore, error) { + if !a.IsValid() { + return nil, fmt.Errorf("%s %w", a, asset.ErrNotSupported) + } + p.m.RLock() defer p.m.RUnlock() c, ok := p.Pairs[a] @@ -48,60 +59,82 @@ func (p *PairsManager) Get(a asset.Item) (*PairStore, error) { return nil, fmt.Errorf("cannot get pair store, %v %w", a, asset.ErrNotSupported) } - return c, nil + return c.copy() } // Store stores a new currency pair config based on its asset type -func (p *PairsManager) Store(a asset.Item, ps PairStore) { +func (p *PairsManager) Store(a asset.Item, ps *PairStore) error { + if !a.IsValid() { + return fmt.Errorf("%s %w", a, asset.ErrNotSupported) + } + cpy, err := ps.copy() + if err != nil { + return err + } p.m.Lock() if p.Pairs == nil { p.Pairs = make(map[asset.Item]*PairStore) } - p.Pairs[a] = &ps + p.Pairs[a] = cpy p.m.Unlock() + return nil } // Delete deletes a map entry based on the supplied asset type func (p *PairsManager) Delete(a asset.Item) { p.m.Lock() - defer p.m.Unlock() - if p.Pairs == nil { - return - } delete(p.Pairs, a) + p.m.Unlock() } // GetPairs gets a list of stored pairs based on the asset type and whether // they're enabled or not func (p *PairsManager) GetPairs(a asset.Item, enabled bool) (Pairs, error) { - p.m.RLock() - defer p.m.RUnlock() - if p.Pairs == nil { - return nil, nil + if !a.IsValid() { + return nil, fmt.Errorf("%s %w", a, asset.ErrNotSupported) } - c, ok := p.Pairs[a] + p.m.RLock() + defer p.m.RUnlock() + pairStore, ok := p.Pairs[a] if !ok { return nil, nil } - if enabled { - for i := range c.Enabled { - if !c.Available.Contains(c.Enabled[i], true) { - return c.Enabled, - fmt.Errorf("enabled pair %s of asset type %s not contained in available list", - c.Enabled[i], - a) - } - } - return c.Enabled, nil + if !enabled { + availPairs := make(Pairs, len(pairStore.Available)) + copy(availPairs, pairStore.Available) + return availPairs, nil } - return c.Available, nil + + lenCheck := len(pairStore.Enabled) + if lenCheck == 0 { + return nil, nil + } + + // NOTE: enabledPairs is declared before the next check for comparison + // reasons within exchange update pairs functionality. + enabledPairs := make(Pairs, lenCheck) + copy(enabledPairs, pairStore.Enabled) + + err := pairStore.Available.ContainsAll(pairStore.Enabled, true) + if err != nil { + err = fmt.Errorf("%w of asset type %s", err, a) + } + return enabledPairs, err } -// StorePairs stores a list of pairs based on the asset type and whether -// they're enabled or not -func (p *PairsManager) StorePairs(a asset.Item, pairs Pairs, enabled bool) { +// StoreFormat stores a new format for request or config format. +func (p *PairsManager) StoreFormat(a asset.Item, pFmt *PairFormat, config bool) error { + if !a.IsValid() { + return fmt.Errorf("%s %w", a, asset.ErrNotSupported) + } + if pFmt == nil { + return errPairFormatIsNil + } + + cpy := *pFmt + p.m.Lock() defer p.m.Unlock() @@ -109,76 +142,130 @@ func (p *PairsManager) StorePairs(a asset.Item, pairs Pairs, enabled bool) { p.Pairs = make(map[asset.Item]*PairStore) } - c, ok := p.Pairs[a] + pairStore, ok := p.Pairs[a] if !ok { - p.Pairs[a] = new(PairStore) - c = p.Pairs[a] + pairStore = new(PairStore) + p.Pairs[a] = pairStore + } + + if config { + pairStore.ConfigFormat = &cpy + } else { + pairStore.RequestFormat = &cpy + } + return nil +} + +// StorePairs stores a list of pairs based on the asset type and whether +// they're enabled or not +func (p *PairsManager) StorePairs(a asset.Item, pairs Pairs, enabled bool) error { + if !a.IsValid() { + return fmt.Errorf("%s %w", a, asset.ErrNotSupported) + } + + // NOTE: Length check not needed in this scenario as it has the ability to + // remove the entire stored list if needed. + cpy := make(Pairs, len(pairs)) + copy(cpy, pairs) + + p.m.Lock() + defer p.m.Unlock() + + if p.Pairs == nil { + p.Pairs = make(map[asset.Item]*PairStore) + } + + pairStore, ok := p.Pairs[a] + if !ok { + pairStore = new(PairStore) + p.Pairs[a] = pairStore } if enabled { - c.Enabled = pairs + pairStore.Enabled = cpy } else { - c.Available = pairs + pairStore.Available = cpy } + + return nil } // DisablePair removes the pair from the enabled pairs list if found func (p *PairsManager) DisablePair(a asset.Item, pair Pair) error { + if !a.IsValid() { + return fmt.Errorf("%s %w", a, asset.ErrNotSupported) + } + + if pair.IsEmpty() { + return ErrCurrencyPairEmpty + } + p.m.Lock() defer p.m.Unlock() - c, err := p.getPairStore(a) + pairStore, err := p.getPairStoreRequiresLock(a) if err != nil { return err } - if !c.Enabled.Contains(pair, true) { - return errors.New("specified pair is not enabled") + enabled, err := pairStore.Enabled.Remove(pair) + if err != nil { + return err } - - c.Enabled = c.Enabled.Remove(pair) + pairStore.Enabled = enabled return nil } // EnablePair adds a pair to the list of enabled pairs if it exists in the list // of available pairs and isn't already added func (p *PairsManager) EnablePair(a asset.Item, pair Pair) error { + if !a.IsValid() { + return fmt.Errorf("%s %w", a, asset.ErrNotSupported) + } + + if pair.IsEmpty() { + return ErrCurrencyPairEmpty + } + p.m.Lock() defer p.m.Unlock() - c, err := p.getPairStore(a) + pairStore, err := p.getPairStoreRequiresLock(a) if err != nil { return err } - if !c.Available.Contains(pair, true) { - return fmt.Errorf("%s %w in the list of available pairs", - pair, ErrPairNotFound) - } - - if c.Enabled.Contains(pair, true) { + if pairStore.Enabled.Contains(pair, true) { return fmt.Errorf("%s %w", pair, ErrPairAlreadyEnabled) } - c.Enabled = c.Enabled.Add(pair) + if !pairStore.Available.Contains(pair, true) { + return fmt.Errorf("%s %w in the list of available pairs", + pair, ErrPairNotFound) + } + pairStore.Enabled = pairStore.Enabled.Add(pair) return nil } // IsAssetEnabled checks to see if an asset is enabled func (p *PairsManager) IsAssetEnabled(a asset.Item) error { + if !a.IsValid() { + return fmt.Errorf("%s %w", a, asset.ErrNotSupported) + } + p.m.RLock() defer p.m.RUnlock() - c, err := p.getPairStore(a) + pairStore, err := p.getPairStoreRequiresLock(a) if err != nil { return err } - if c.AssetEnabled == nil { + if pairStore.AssetEnabled == nil { return fmt.Errorf("%s %w", a, ErrAssetIsNil) } - if !*c.AssetEnabled { + if !*pairStore.AssetEnabled { return fmt.Errorf("%s %w", a, errAssetNotEnabled) } return nil @@ -186,44 +273,48 @@ func (p *PairsManager) IsAssetEnabled(a asset.Item) error { // SetAssetEnabled sets if an asset is enabled or disabled for first run func (p *PairsManager) SetAssetEnabled(a asset.Item, enabled bool) error { + if !a.IsValid() { + return fmt.Errorf("%s %w", a, asset.ErrNotSupported) + } + p.m.Lock() defer p.m.Unlock() - c, err := p.getPairStore(a) + pairStore, err := p.getPairStoreRequiresLock(a) if err != nil { return err } - if c.AssetEnabled == nil { - c.AssetEnabled = convert.BoolPtr(enabled) + if pairStore.AssetEnabled == nil { + pairStore.AssetEnabled = convert.BoolPtr(enabled) return nil } - if !*c.AssetEnabled && !enabled { + if !*pairStore.AssetEnabled && !enabled { return errors.New("asset already disabled") - } else if *c.AssetEnabled && enabled { + } else if *pairStore.AssetEnabled && enabled { return ErrAssetAlreadyEnabled } - *c.AssetEnabled = enabled + *pairStore.AssetEnabled = enabled return nil } -func (p *PairsManager) getPairStore(a asset.Item) (*PairStore, error) { +func (p *PairsManager) getPairStoreRequiresLock(a asset.Item) (*PairStore, error) { if p.Pairs == nil { return nil, errors.New("pair manager not initialised") } - c, ok := p.Pairs[a] + pairStore, ok := p.Pairs[a] if !ok { return nil, errors.New("asset type not found") } - if c == nil { + if pairStore == nil { return nil, errors.New("currency store is nil") } - return c, nil + return pairStore, nil } // UnmarshalJSON implements the unmarshal json interface so that the key can be @@ -255,3 +346,40 @@ func (fs FullStore) MarshalJSON() ([]byte, error) { } return json.Marshal(temp) } + +// copy copies and segregates pair store from internal and external calls. +func (ps *PairStore) copy() (*PairStore, error) { + if ps == nil { + return nil, errPairStoreIsNil + } + var assetEnabled *bool + if ps.AssetEnabled != nil { + assetEnabled = convert.BoolPtr(*ps.AssetEnabled) + } + + enabled := make(Pairs, len(ps.Enabled)) + copy(enabled, ps.Enabled) + + avail := make(Pairs, len(ps.Available)) + copy(avail, ps.Available) + + var rFmt *PairFormat + if ps.RequestFormat != nil { + cpy := *ps.RequestFormat + rFmt = &cpy + } + + var cFmt *PairFormat + if ps.ConfigFormat != nil { + cpy := *ps.ConfigFormat + cFmt = &cpy + } + + return &PairStore{ + AssetEnabled: assetEnabled, + Enabled: enabled, + Available: avail, + RequestFormat: rFmt, + ConfigFormat: cFmt, + }, nil +} diff --git a/currency/manager_test.go b/currency/manager_test.go index 7c4c59f4..514bd222 100644 --- a/currency/manager_test.go +++ b/currency/manager_test.go @@ -9,9 +9,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/asset" ) -var p PairsManager - -func initTest(t *testing.T) { +func initTest(t *testing.T) *PairsManager { t.Helper() spotAvailable, err := NewPairsFromStrings([]string{"BTC-USD", "LTC-USD"}) if err != nil { @@ -23,7 +21,7 @@ func initTest(t *testing.T) { t.Fatal(err) } - spot := PairStore{ + spot := &PairStore{ AssetEnabled: convert.BoolPtr(true), Available: spotAvailable, Enabled: spotEnabled, @@ -31,7 +29,7 @@ func initTest(t *testing.T) { ConfigFormat: &PairFormat{Uppercase: true, Delimiter: "-"}, } - futures := PairStore{ + futures := &PairStore{ AssetEnabled: convert.BoolPtr(false), Available: spotAvailable, Enabled: spotEnabled, @@ -39,12 +37,23 @@ func initTest(t *testing.T) { ConfigFormat: &PairFormat{Uppercase: true, Delimiter: "-"}, } - p.Store(asset.Spot, spot) - p.Store(asset.Futures, futures) + var p PairsManager + + err = p.Store(asset.Spot, spot) + if err != nil { + t.Fatal(err) + } + err = p.Store(asset.Futures, futures) + if err != nil { + t.Fatal(err) + } + + return &p } func TestGetAssetTypes(t *testing.T) { - initTest(t) + t.Parallel() + p := initTest(t) a := p.GetAssetTypes(false) if len(a) != 2 { @@ -62,20 +71,27 @@ func TestGetAssetTypes(t *testing.T) { } func TestGet(t *testing.T) { - initTest(t) + t.Parallel() + p := initTest(t) _, err := p.Get(asset.Spot) if err != nil { t.Error(err) } + _, err = p.Get(asset.Empty) + if !errors.Is(err, asset.ErrNotSupported) { + t.Fatalf("received: '%v' bu expected: '%v'", err, asset.ErrNotSupported) + } + _, err = p.Get(asset.CoinMarginedFutures) - if err == nil { - t.Error("CoinMarginedFutures should be nil") + if !errors.Is(err, asset.ErrNotSupported) { + t.Fatalf("received: '%v' bu expected: '%v'", err, asset.ErrNotSupported) } } func TestStore(t *testing.T) { + t.Parallel() availPairs, err := NewPairsFromStrings([]string{"BTC-USD", "LTC-USD"}) if err != nil { t.Fatal(err) @@ -86,8 +102,10 @@ func TestStore(t *testing.T) { t.Fatal(err) } - p.Store(asset.Futures, - PairStore{ + p := initTest(t) + + err = p.Store(asset.Futures, + &PairStore{ Available: availPairs, Enabled: enabledPairs, RequestFormat: &PairFormat{ @@ -99,6 +117,9 @@ func TestStore(t *testing.T) { }, }, ) + if err != nil { + t.Fatal(err) + } f, err := p.Get(asset.Futures) if err != nil { @@ -108,9 +129,22 @@ func TestStore(t *testing.T) { if f == nil { t.Error("Futures assets shouldn't be nil") } + + err = p.Store(asset.Empty, nil) + if !errors.Is(err, asset.ErrNotSupported) { + t.Fatalf("received: '%v' bu expected: '%v'", err, asset.ErrNotSupported) + } + + err = p.Store(asset.Futures, nil) + if !errors.Is(err, errPairStoreIsNil) { + t.Fatalf("received: '%v' bu expected: '%v'", err, errPairStoreIsNil) + } } func TestDelete(t *testing.T) { + t.Parallel() + p := initTest(t) + p.Pairs = nil p.Delete(asset.Spot) @@ -119,9 +153,10 @@ func TestDelete(t *testing.T) { t.Fatal(err) } - p.Store(asset.Spot, PairStore{ - Available: btcusdPairs, - }) + err = p.Store(asset.Spot, &PairStore{Available: btcusdPairs}) + if err != nil { + t.Fatal(err) + } p.Delete(asset.UpsideProfitContract) spotPS, err := p.Get(asset.Spot) @@ -141,6 +176,9 @@ func TestDelete(t *testing.T) { } func TestGetPairs(t *testing.T) { + t.Parallel() + p := initTest(t) + p.Pairs = nil pairs, err := p.GetPairs(asset.Spot, true) if err != nil { @@ -151,7 +189,7 @@ func TestGetPairs(t *testing.T) { t.Fatal("pairs shouldn't be populated") } - initTest(t) + p = initTest(t) pairs, err = p.GetPairs(asset.Spot, true) if err != nil { t.Fatal(err) @@ -161,8 +199,8 @@ func TestGetPairs(t *testing.T) { } pairs, err = p.GetPairs(asset.Empty, true) - if err != nil { - t.Fatal(err) + if !errors.Is(err, asset.ErrNotSupported) { + t.Fatalf("received: '%v' but expetced: '%v'", err, asset.ErrNotSupported) } if pairs != nil { @@ -179,7 +217,57 @@ func TestGetPairs(t *testing.T) { } } +func TestStoreFormat(t *testing.T) { + t.Parallel() + p := &PairsManager{} + + err := p.StoreFormat(0, &PairFormat{Delimiter: "~"}, true) + if !errors.Is(err, asset.ErrNotSupported) { + t.Fatalf("received: %v but expected: %v", err, asset.ErrNotSupported) + } + + err = p.StoreFormat(asset.Spot, nil, true) + if !errors.Is(err, errPairFormatIsNil) { + t.Fatalf("received: %v but expected: %v", err, errPairFormatIsNil) + } + + err = p.StoreFormat(asset.Spot, &PairFormat{Delimiter: "~"}, true) + if !errors.Is(err, nil) { + t.Fatalf("received: %v but expected: %v", err, nil) + } + ps, err := p.Get(asset.Spot) + if err != nil { + t.Fatal(err) + } + + if ps.ConfigFormat.Delimiter != "~" { + t.Fatal("unexpected value") + } + + err = p.StoreFormat(asset.Spot, &PairFormat{Delimiter: "/"}, false) + if !errors.Is(err, nil) { + t.Fatalf("received: %v but expected: %v", err, nil) + } + + ps, err = p.Get(asset.Spot) + if err != nil { + t.Fatal(err) + } + + if ps.RequestFormat.Delimiter != "/" { + t.Fatal("unexpected value") + } +} + func TestStorePairs(t *testing.T) { + t.Parallel() + p := initTest(t) + + err := p.StorePairs(0, nil, false) + if !errors.Is(err, asset.ErrNotSupported) { + t.Fatalf("received: %v but expected: %v", err, asset.ErrNotSupported) + } + p.Pairs = nil ethusdPairs, err := NewPairsFromStrings([]string{"ETH-USD"}) @@ -187,7 +275,11 @@ func TestStorePairs(t *testing.T) { t.Fatal(err) } - p.StorePairs(asset.Spot, ethusdPairs, false) + err = p.StorePairs(asset.Spot, ethusdPairs, false) + if !errors.Is(err, nil) { + t.Fatalf("received: %v but expected: %v", err, nil) + } + pairs, err := p.GetPairs(asset.Spot, false) if err != nil { t.Fatal(err) @@ -202,8 +294,11 @@ func TestStorePairs(t *testing.T) { t.Errorf("TestStorePairs failed, unexpected result") } - initTest(t) - p.StorePairs(asset.Spot, ethusdPairs, false) + p = initTest(t) + err = p.StorePairs(asset.Spot, ethusdPairs, false) + if !errors.Is(err, nil) { + t.Fatalf("received: %v but expected: %v", err, nil) + } pairs, err = p.GetPairs(asset.Spot, false) if err != nil { t.Fatal(err) @@ -222,8 +317,16 @@ func TestStorePairs(t *testing.T) { t.Error(err) } - p.StorePairs(asset.Futures, ethkrwPairs, true) - p.StorePairs(asset.Futures, ethkrwPairs, false) + err = p.StorePairs(asset.Futures, ethkrwPairs, true) + if !errors.Is(err, nil) { + t.Fatalf("received: %v but expected: %v", err, nil) + } + + err = p.StorePairs(asset.Futures, ethkrwPairs, false) + if !errors.Is(err, nil) { + t.Fatalf("received: %v but expected: %v", err, nil) + } + pairs, err = p.GetPairs(asset.Futures, true) if err != nil { t.Fatal(err) @@ -244,6 +347,17 @@ func TestStorePairs(t *testing.T) { } func TestDisablePair(t *testing.T) { + t.Parallel() + p := initTest(t) + + if err := p.DisablePair(asset.Empty, EMPTYPAIR); !errors.Is(err, asset.ErrNotSupported) { + t.Fatalf("received: '%v' but expected: '%v'", err, asset.ErrNotSupported) + } + + if err := p.DisablePair(asset.Spot, EMPTYPAIR); !errors.Is(err, ErrCurrencyPairEmpty) { + t.Fatalf("received: '%v' but expected: '%v'", err, ErrCurrencyPairEmpty) + } + p.Pairs = nil // Test disabling a pair when the pair manager is not initialised if err := p.DisablePair(asset.Spot, NewPair(BTC, USD)); err == nil { @@ -251,7 +365,7 @@ func TestDisablePair(t *testing.T) { } // Test asset type which doesn't exist - initTest(t) + p = initTest(t) if err := p.DisablePair(asset.Futures, EMPTYPAIR); err == nil { t.Error("unexpected result") } @@ -263,7 +377,7 @@ func TestDisablePair(t *testing.T) { } // Test disabling a pair which isn't enabled - initTest(t) + p = initTest(t) if err := p.DisablePair(asset.Spot, NewPair(LTC, USD)); err == nil { t.Error("unexpected result") } @@ -275,6 +389,13 @@ func TestDisablePair(t *testing.T) { } func TestEnablePair(t *testing.T) { + t.Parallel() + p := initTest(t) + + if err := p.EnablePair(asset.Empty, NewPair(BTC, USD)); !errors.Is(err, asset.ErrNotSupported) { + t.Fatalf("received: '%v' but expected: '%v'", err, asset.ErrNotSupported) + } + p.Pairs = nil // Test enabling a pair when the pair manager is not initialised if err := p.EnablePair(asset.Spot, NewPair(BTC, USD)); err == nil { @@ -282,7 +403,7 @@ func TestEnablePair(t *testing.T) { } // Test asset type which doesn't exist - initTest(t) + p = initTest(t) if err := p.EnablePair(asset.Futures, EMPTYPAIR); err == nil { t.Error("unexpected result") } @@ -294,7 +415,7 @@ func TestEnablePair(t *testing.T) { } // Test enabling a pair which isn't in the list of available pairs - initTest(t) + p = initTest(t) if err := p.EnablePair(asset.Spot, NewPair(ETH, USD)); err == nil { t.Error("unexpected result") } @@ -311,9 +432,16 @@ func TestEnablePair(t *testing.T) { } func TestIsAssetEnabled_SetAssetEnabled(t *testing.T) { + t.Parallel() + p := initTest(t) + + err := p.IsAssetEnabled(asset.Empty) + if !errors.Is(err, asset.ErrNotSupported) { + t.Fatalf("received: '%v' but expected: '%v'", err, asset.ErrNotSupported) + } p.Pairs = nil // Test enabling a pair when the pair manager is not initialised - err := p.IsAssetEnabled(asset.Spot) + err = p.IsAssetEnabled(asset.Spot) if err == nil { t.Error("unexpected result") } @@ -324,7 +452,7 @@ func TestIsAssetEnabled_SetAssetEnabled(t *testing.T) { } // Test asset type which doesn't exist - initTest(t) + p = initTest(t) p.Pairs[asset.Spot].AssetEnabled = nil diff --git a/currency/pair.go b/currency/pair.go index 13be4a0c..64e8f2b6 100644 --- a/currency/pair.go +++ b/currency/pair.go @@ -112,8 +112,8 @@ func NewPairFromFormattedPairs(currencyPair string, pairs Pairs, pairFmt PairFor } // Format formats the given pair as a string -func (f *PairFormat) Format(pair Pair) string { - return pair.Format(f.Delimiter, f.Uppercase).String() +func (f PairFormat) Format(pair Pair) string { + return pair.Format(f).String() } // MatchPairsWithNoDelimiter will move along a predictable index on the provided currencyPair @@ -123,7 +123,7 @@ func (f *PairFormat) Format(pair Pair) string { // infer where the delimiter is located eg BETHERETH is BETHER ETH func MatchPairsWithNoDelimiter(currencyPair string, pairs Pairs, pairFmt PairFormat) (Pair, error) { for i := range pairs { - fPair := pairs[i].Format(pairFmt.Delimiter, pairFmt.Uppercase) + fPair := pairs[i].Format(pairFmt) maxLen := 6 if len(currencyPair) < maxLen { maxLen = len(currencyPair) diff --git a/currency/pair_methods.go b/currency/pair_methods.go index 41460597..841c219f 100644 --- a/currency/pair_methods.go +++ b/currency/pair_methods.go @@ -4,6 +4,9 @@ import ( "encoding/json" ) +// EMPTYFORMAT defines an empty pair format +var EMPTYFORMAT = PairFormat{} + // String returns a currency pair string func (p Pair) String() string { return p.Base.String() + p.Delimiter + p.Quote.String() @@ -47,9 +50,9 @@ func (p Pair) MarshalJSON() ([]byte, error) { // Format changes the currency based on user preferences overriding the default // String() display -func (p Pair) Format(delimiter string, uppercase bool) Pair { - p.Delimiter = delimiter - if uppercase { +func (p Pair) Format(pf PairFormat) Pair { + p.Delimiter = pf.Delimiter + if pf.Uppercase { return p.Upper() } return p.Lower() diff --git a/currency/pair_test.go b/currency/pair_test.go index 771cda7f..afa40bfe 100644 --- a/currency/pair_test.go +++ b/currency/pair_test.go @@ -210,7 +210,7 @@ func TestDisplay(t *testing.T) { ) } - actual = pair.Format("", false).String() + actual = EMPTYFORMAT.Format(pair) expected = "btcusd" if actual != expected { t.Errorf( @@ -219,7 +219,7 @@ func TestDisplay(t *testing.T) { ) } - actual = pair.Format("~", true).String() + actual = pair.Format(PairFormat{Delimiter: "~", Uppercase: true}).String() expected = "BTC~USD" if actual != expected { t.Errorf( @@ -529,7 +529,7 @@ func TestNewPairFromFormattedPairs(t *testing.T) { } // Now a wrong one, will default to NewPairFromString - p, err = NewPairFromFormattedPairs("ethusdt", pairs, PairFormat{}) + p, err = NewPairFromFormattedPairs("ethusdt", pairs, EMPTYFORMAT) if err != nil { t.Fatal(err) } @@ -618,34 +618,75 @@ func TestFindPairDifferences(t *testing.T) { } // Test new pair update - newPairs, removedPairs := pairList.FindDifferences(dash) - if len(newPairs) != 1 && len(removedPairs) != 3 { + diff, err := pairList.FindDifferences(dash, PairFormat{Delimiter: DashDelimiter, Uppercase: true}) + if err != nil { + t.Fatal(err) + } + if len(diff.New) != 1 && len(diff.Remove) != 3 && diff.FormatDifference { t.Error("TestFindPairDifferences: Unexpected values") } - emptyPairsList, err := NewPairsFromStrings([]string{""}) - if !errors.Is(err, errCannotCreatePair) { - t.Fatalf("received: '%v' but expected: '%v'", err, errCannotCreatePair) + diff, err = pairList.FindDifferences(Pairs{}, EMPTYFORMAT) + if err != nil { + t.Fatal(err) } - - // Test that we don't allow empty strings for new pairs - newPairs, removedPairs = pairList.FindDifferences(emptyPairsList) - if len(newPairs) != 0 && len(removedPairs) != 3 { + if len(diff.New) != 0 && len(diff.Remove) != 3 && !diff.FormatDifference { t.Error("TestFindPairDifferences: Unexpected values") } - // Test that we don't allow empty strings for new pairs - newPairs, removedPairs = emptyPairsList.FindDifferences(pairList) - if len(newPairs) != 3 && len(removedPairs) != 0 { + diff, err = Pairs{}.FindDifferences(pairList, EMPTYFORMAT) + if err != nil { + t.Fatal(err) + } + if len(diff.New) != 3 && len(diff.Remove) != 0 && diff.FormatDifference { t.Error("TestFindPairDifferences: Unexpected values") } // Test that the supplied pair lists are the same, so // no newPairs or removedPairs - newPairs, removedPairs = pairList.FindDifferences(pairList) - if len(newPairs) != 0 && len(removedPairs) != 0 { + diff, err = pairList.FindDifferences(pairList, PairFormat{Delimiter: DashDelimiter, Uppercase: true}) + if err != nil { + t.Fatal(err) + } + if len(diff.New) != 0 && len(diff.Remove) != 0 && !diff.FormatDifference { t.Error("TestFindPairDifferences: Unexpected values") } + + _, err = pairList.FindDifferences(Pairs{EMPTYPAIR}, EMPTYFORMAT) + if !errors.Is(err, ErrCurrencyPairEmpty) { + t.Fatalf("received: '%v' but expected: '%v'", err, ErrCurrencyPairEmpty) + } + + _, err = Pairs{EMPTYPAIR}.FindDifferences(pairList, EMPTYFORMAT) + if !errors.Is(err, ErrCurrencyPairEmpty) { + t.Fatalf("received: '%v' but expected: '%v'", err, ErrCurrencyPairEmpty) + } + + // Test duplication + duplication, err := NewPairsFromStrings([]string{defaultPairWDelimiter, "ETH-USD", "LTC-USD", "ETH-USD"}) + if err != nil { + t.Fatal(err) + } + + _, err = pairList.FindDifferences(duplication, EMPTYFORMAT) + if !errors.Is(err, ErrPairDuplication) { + t.Fatalf("received: '%v' but expected: '%v'", err, ErrPairDuplication) + } + + // This will allow for the removal of the duplicated item to be returned if + // contained in the original list. + diff, err = duplication.FindDifferences(pairList, EMPTYFORMAT) + if !errors.Is(err, nil) { + t.Fatalf("received: '%v' but expected: '%v'", err, nil) + } + + if len(diff.Remove) != 1 { + t.Fatal("expected removal value in pair difference struct") + } + + if !diff.Remove[0].Equal(pairList[1]) { + t.Fatal("unexpected value returned", diff.Remove[0], pairList[1]) + } } func TestPairsToStringArray(t *testing.T) { @@ -663,7 +704,10 @@ func TestPairsToStringArray(t *testing.T) { func TestRandomPairFromPairs(t *testing.T) { // Test that an empty pairs array returns an empty currency pair var emptyPairs Pairs - result := emptyPairs.GetRandomPair() + result, err := emptyPairs.GetRandomPair() + if !errors.Is(err, ErrCurrencyPairsEmpty) { + t.Fatalf("received: '%v' but expected: '%v'", err, ErrCurrencyPairsEmpty) + } if !result.IsEmpty() { t.Error("TestRandomPairFromPairs: Unexpected values") } @@ -671,7 +715,10 @@ func TestRandomPairFromPairs(t *testing.T) { // Test that a populated pairs array returns a non-empty currency pair var pairs Pairs pairs = append(pairs, NewPair(BTC, USD)) - result = pairs.GetRandomPair() + result, err = pairs.GetRandomPair() + if !errors.Is(err, nil) { + t.Fatalf("received: '%v' but expected: '%v'", err, nil) + } if result.IsEmpty() { t.Error("TestRandomPairFromPairs: Unexpected values") @@ -682,16 +729,15 @@ func TestRandomPairFromPairs(t *testing.T) { pairs = append(pairs, NewPair(ETH, USD)) expectedResults := make(map[string]bool) for i := 0; i < 50; i++ { - p := pairs.GetRandomPair().String() - _, ok := expectedResults[p] - if !ok { - expectedResults[p] = true + result, err = pairs.GetRandomPair() + if !errors.Is(err, nil) { + t.Fatalf("received: '%v' but expected: '%v'", err, nil) } + expectedResults[result.String()] = true } for x := range pairs { - _, ok := expectedResults[pairs[x].String()] - if !ok { + if !expectedResults[pairs[x].String()] { t.Error("TestRandomPairFromPairs: Unexpected values") } } diff --git a/currency/pair_types.go b/currency/pair_types.go index d268dee6..a880bdf0 100644 --- a/currency/pair_types.go +++ b/currency/pair_types.go @@ -9,3 +9,11 @@ type Pair struct { // Pairs defines a list of pairs type Pairs []Pair + +// PairDifference defines the difference between a set of pairs including a +// change in format. +type PairDifference struct { + New Pairs + Remove Pairs + FormatDifference bool +} diff --git a/currency/pairs.go b/currency/pairs.go index c4cee7d1..211b4653 100644 --- a/currency/pairs.go +++ b/currency/pairs.go @@ -14,6 +14,10 @@ var ( errSymbolEmpty = errors.New("symbol is empty") errPairsEmpty = errors.New("pairs are empty") errNoDelimiter = errors.New("no delimiter was supplied") + + // ErrPairDuplication defines an error when there is multiple of the same + // currency pairs found. + ErrPairDuplication = errors.New("currency pair duplication") ) // NewPairsFromStrings takes in currency pair strings and returns a currency @@ -54,24 +58,22 @@ func (p Pairs) Join() string { } // Format formats the pair list to the exchange format configuration -func (p Pairs) Format(delimiter, index string, uppercase bool) Pairs { - pairs := make(Pairs, 0, len(p)) +func (p Pairs) Format(pairFmt PairFormat) Pairs { + pairs := make(Pairs, len(p)) + copy(pairs, p) + var err error - for _, format := range p { - if index != "" { - format, err = NewPairFromIndex(format.String(), index) + for x := range pairs { + if pairFmt.Index != "" { + pairs[x], err = NewPairFromIndex(p[x].String(), pairFmt.Index) if err != nil { - log.Errorf(log.Global, - "failed to create NewPairFromIndex. Err: %s\n", err) - continue + log.Errorf(log.Global, "failed to create NewPairFromIndex. Err: %s\n", err) + return nil } } - format.Delimiter = delimiter - if uppercase { - pairs = append(pairs, format.Upper()) - } else { - pairs = append(pairs, format.Lower()) - } + pairs[x].Base.UpperCase = pairFmt.Uppercase + pairs[x].Quote.UpperCase = pairFmt.Uppercase + pairs[x].Delimiter = pairFmt.Delimiter } return pairs } @@ -120,19 +122,45 @@ func (p Pairs) Lower() Pairs { // array func (p Pairs) Contains(check Pair, exact bool) bool { for i := range p { - if exact { - if p[i].Equal(check) { - return true - } - } else { - if p[i].EqualIncludeReciprocal(check) { - return true - } + if (exact && p[i].Equal(check)) || + (!exact && p[i].EqualIncludeReciprocal(check)) { + return true } } return false } +// ContainsAll checks to see if all pairs supplied are contained within the +// original pairs list. +func (p Pairs) ContainsAll(check Pairs, exact bool) error { + if len(check) == 0 { + return errPairsEmpty + } + + comparative := make(Pairs, len(p)) + copy(comparative, p) +list: + for x := range check { + for y := range comparative { + if (exact && check[x].Equal(comparative[y])) || + (!exact && check[x].EqualIncludeReciprocal(comparative[y])) { + // Reduce list size to decrease array traversal speed on iteration. + comparative[y] = comparative[len(comparative)-1] + comparative = comparative[:len(comparative)-1] + continue list + } + } + + // Opted for in error original check for duplication. + if p.Contains(check[x], exact) { + return fmt.Errorf("%s %w", check[x], ErrPairDuplication) + } + + return fmt.Errorf("%s %w", check[x], ErrPairNotContainedInAvailablePairs) + } + return nil +} + // ContainsCurrency checks to see if a specified currency code exists inside a // currency pair array func (p Pairs) ContainsCurrency(check Code) bool { @@ -184,15 +212,15 @@ func (p Pairs) GetPairsByCurrencies(currencies Currencies) Pairs { } // Remove removes the specified pair from the list of pairs if it exists -func (p Pairs) Remove(pair Pair) Pairs { - pairs := make(Pairs, 0, len(p)) +func (p Pairs) Remove(pair Pair) (Pairs, error) { + pairs := make(Pairs, len(p)) + copy(pairs, p) for x := range p { if p[x].Equal(pair) { - continue + return append(pairs[:x], pairs[x+1:]...), nil } - pairs = append(pairs, p[x]) } - return pairs + return nil, fmt.Errorf("%s %w", pair, ErrPairNotFound) } // Add adds a specified pair to the list of pairs if it doesn't exist @@ -217,32 +245,59 @@ func (p Pairs) GetMatch(pair Pair) (Pair, error) { } // FindDifferences returns pairs which are new or have been removed -func (p Pairs) FindDifferences(pairs Pairs) (newPairs, removedPairs Pairs) { - for x := range pairs { - if pairs[x].String() == "" { - continue +func (p Pairs) FindDifferences(incoming Pairs, pairFmt PairFormat) (PairDifference, error) { + newPairs := make(Pairs, 0, len(incoming)) + check := make(map[string]bool) + for x := range incoming { + if incoming[x].IsEmpty() { + return PairDifference{}, fmt.Errorf("contained in the incoming pairs a %w", ErrCurrencyPairEmpty) } - if !p.Contains(pairs[x], true) { - newPairs = append(newPairs, pairs[x]) + format := EMPTYFORMAT.Format(incoming[x]) + if check[format] { + return PairDifference{}, fmt.Errorf("contained in the incoming pairs %w", ErrPairDuplication) + } + check[format] = true + if !p.Contains(incoming[x], true) { + newPairs = append(newPairs, incoming[x]) } } + removedPairs := make(Pairs, 0, len(p)) + check = make(map[string]bool) for x := range p { - if p[x].String() == "" { - continue + if p[x].IsEmpty() { + return PairDifference{}, fmt.Errorf("contained in the existing pairs a %w", ErrCurrencyPairEmpty) } - if !pairs.Contains(p[x], true) { + format := EMPTYFORMAT.Format(p[x]) + if !incoming.Contains(p[x], true) || check[format] { removedPairs = append(removedPairs, p[x]) } + check[format] = true } - return + return PairDifference{ + New: newPairs, + Remove: removedPairs, + FormatDifference: p.HasFormatDifference(pairFmt), + }, nil +} + +// HasFormatDifference checks and validates full formatting across a pairs list +func (p Pairs) HasFormatDifference(pairFmt PairFormat) bool { + for x := range p { + if p[x].Delimiter != pairFmt.Delimiter || + (!p[x].Base.IsEmpty() && p[x].Base.UpperCase != pairFmt.Uppercase) || + (!p[x].Quote.IsEmpty() && p[x].Quote.UpperCase != pairFmt.Uppercase) { + return true + } + } + return false } // GetRandomPair returns a random pair from a list of pairs -func (p Pairs) GetRandomPair() Pair { - if pairsLen := len(p); pairsLen != 0 { - return p[rand.Intn(pairsLen)] //nolint:gosec // basic number generation required, no need for crypo/rand +func (p Pairs) GetRandomPair() (Pair, error) { + if len(p) == 0 { + return EMPTYPAIR, ErrCurrencyPairsEmpty } - return EMPTYPAIR + return p[rand.Intn(len(p))], nil //nolint:gosec // basic number generation required, no need for crypo/rand } // DeriveFrom matches symbol string to the available pairs list when no @@ -355,3 +410,33 @@ func (p Pairs) GetStablesMatch(code Code) Pairs { } return stablePairs } + +// ValidateAndConform checks for duplications and empty pairs then conforms the +// entire pairs list to the supplied formatting (unless bypassed). +// Map[string]bool type is used to make sure delimiters are not included so +// different formatting entry duplications can be found e.g. `LINKUSDTM21`, +// `LIN-KUSDTM21` or `LINK-USDTM21 are all the same instances but with different +// unintentional processes for formatting. +func (p Pairs) ValidateAndConform(pFmt PairFormat, bypassFormatting bool) (Pairs, error) { + processedPairs := make(map[string]bool, len(p)) + formatted := make(Pairs, len(p)) + var target int + for x := range p { + if p[x].IsEmpty() { + return nil, fmt.Errorf("cannot update pairs %w", ErrCurrencyPairEmpty) + } + strippedPair := EMPTYFORMAT.Format(p[x]) + if processedPairs[strippedPair] { + return nil, fmt.Errorf("cannot update pairs %w with [%s]", ErrPairDuplication, p[x]) + } + // Force application of supplied formatting + processedPairs[strippedPair] = true + if !bypassFormatting { + formatted[target] = p[x].Format(pFmt) + } else { + formatted[target] = p[x] + } + target++ + } + return formatted, nil +} diff --git a/currency/pairs_test.go b/currency/pairs_test.go index 85523af0..0693dffb 100644 --- a/currency/pairs_test.go +++ b/currency/pairs_test.go @@ -96,20 +96,23 @@ func TestPairsFormat(t *testing.T) { } expected := "BTC-USD,BTC-AUD,BTC-LTC" - if pairs.Format("-", "", true).Join() != expected { + formatting := PairFormat{Delimiter: "-", Index: "", Uppercase: true} + if pairs.Format(formatting).Join() != expected { t.Errorf("Pairs Join() error expected %s but received %s", - expected, pairs.Format("-", "", true).Join()) + expected, pairs.Format(formatting).Join()) } expected = "btc:usd,btc:aud,btc:ltc" - if pairs.Format(":", "", false).Join() != expected { + formatting = PairFormat{Delimiter: ":", Index: "", Uppercase: false} + if pairs.Format(formatting).Join() != expected { t.Errorf("Pairs Join() error expected %s but received %s", - expected, pairs.Format(":", "", false).Join()) + expected, pairs.Format(formatting).Join()) } - if pairs.Format(":", "KRW", false).Join() != "" { + formatting = PairFormat{Delimiter: ":", Index: "KRW", Uppercase: false} + if pairs.Format(formatting).Join() != "" { t.Errorf("Pairs Join() error expected %s but received %s", - expected, pairs.Format(":", "KRW", true).Join()) + expected, pairs.Format(formatting).Join()) } pairs, err = NewPairsFromStrings([]string{"DASHKRW", "BTCKRW"}) @@ -117,9 +120,10 @@ func TestPairsFormat(t *testing.T) { t.Fatal(err) } expected = "dash-krw,btc-krw" - if pairs.Format("-", "KRW", false).Join() != expected { + formatting = PairFormat{Delimiter: "-", Index: "KRW", Uppercase: false} + if pairs.Format(formatting).Join() != expected { t.Errorf("Pairs Join() error expected %s but received %s", - expected, pairs.Format("-", "KRW", false).Join()) + expected, pairs.Format(formatting).Join()) } } @@ -220,17 +224,68 @@ func TestGetPairsByFilter(t *testing.T) { func TestRemove(t *testing.T) { t.Parallel() - var pairs = Pairs{ + var oldPairs = Pairs{ NewPair(BTC, USD), NewPair(LTC, USD), NewPair(LTC, USDT), } + compare := make(Pairs, len(oldPairs)) + copy(compare, oldPairs) + p := NewPair(BTC, USD) - pairs = pairs.Remove(p) - if pairs.Contains(p, true) || len(pairs) != 2 { + newPairs, err := oldPairs.Remove(p) + if !errors.Is(err, nil) { + t.Fatalf("received: '%v' but expected '%v'", err, nil) + } + + err = compare.ContainsAll(oldPairs, true) + if err != nil { + t.Fatal(err) + } + + if newPairs.Contains(p, true) || len(newPairs) != 2 { t.Error("TestRemove unexpected result") } + + _, err = newPairs.Remove(p) + if !errors.Is(err, ErrPairNotFound) { + t.Fatalf("received: '%v' but expected '%v'", err, ErrPairNotFound) + } + + newPairs, err = oldPairs.Remove(p) + if !errors.Is(err, nil) { + t.Fatalf("received: '%v' but expected '%v'", err, nil) + } + + newPairs, err = newPairs.Remove(NewPair(LTC, USD)) + if !errors.Is(err, nil) { + t.Fatalf("received: '%v' but expected '%v'", err, nil) + } + + err = compare.ContainsAll(oldPairs, true) + if err != nil { + t.Fatal(err) + } + + _, err = newPairs.Remove(NewPair(LTC, USD)) + if !errors.Is(err, ErrPairNotFound) { + t.Fatalf("received: '%v' but expected '%v'", err, ErrPairNotFound) + } + + newPairs, err = newPairs.Remove(NewPair(LTC, USDT)) + if !errors.Is(err, nil) { + t.Fatalf("received: '%v' but expected '%v'", err, nil) + } + + if len(newPairs) != 0 { + t.Error("unexpected value") + } + + _, err = newPairs.Remove(NewPair(LTC, USDT)) + if !errors.Is(err, ErrPairNotFound) { + t.Fatalf("received: '%v' but expected '%v'", err, ErrPairNotFound) + } } func TestAdd(t *testing.T) { @@ -280,6 +335,62 @@ func TestContains(t *testing.T) { } } +func TestContainsAll(t *testing.T) { + t.Parallel() + var pairs = Pairs{ + NewPair(BTC, USD), + NewPair(LTC, USD), + NewPair(USD, ZRX), + } + + err := pairs.ContainsAll(nil, true) + if !errors.Is(err, errPairsEmpty) { + t.Fatalf("received: '%v' but expected: '%v'", err, errPairsEmpty) + } + + err = pairs.ContainsAll(Pairs{NewPair(BTC, USD)}, true) + if !errors.Is(err, nil) { + t.Fatalf("received: '%v' but expected: '%v'", err, nil) + } + + err = pairs.ContainsAll(Pairs{NewPair(USD, BTC)}, false) + if !errors.Is(err, nil) { + t.Fatalf("received: '%v' but expected: '%v'", err, nil) + } + + err = pairs.ContainsAll(Pairs{NewPair(XRP, BTC)}, false) + if !errors.Is(err, ErrPairNotContainedInAvailablePairs) { + t.Fatalf("received: '%v' but expected: '%v'", err, ErrPairNotContainedInAvailablePairs) + } + + err = pairs.ContainsAll(Pairs{NewPair(XRP, BTC)}, true) + if !errors.Is(err, ErrPairNotContainedInAvailablePairs) { + t.Fatalf("received: '%v' but expected: '%v'", err, ErrPairNotContainedInAvailablePairs) + } + + err = pairs.ContainsAll(pairs, true) + if !errors.Is(err, nil) { + t.Fatalf("received: '%v' but expected: '%v'", err, nil) + } + + err = pairs.ContainsAll(pairs, false) + if !errors.Is(err, nil) { + t.Fatalf("received: '%v' but expected: '%v'", err, nil) + } + + var duplication = Pairs{ + NewPair(BTC, USD), + NewPair(LTC, USD), + NewPair(USD, ZRX), + NewPair(USD, ZRX), + } + + err = pairs.ContainsAll(duplication, false) + if !errors.Is(err, ErrPairDuplication) { + t.Fatalf("received: '%v' but expected: '%v'", err, ErrPairDuplication) + } +} + func TestDeriveFrom(t *testing.T) { t.Parallel() _, err := Pairs{}.DeriveFrom("") @@ -502,8 +613,10 @@ func BenchmarkPairsFormat(b *testing.B) { NewPair(DAI, XRP), } + formatting := PairFormat{Delimiter: "/", Index: "", Uppercase: false} + for x := 0; x < b.N; x++ { - _ = pairs.Format("/", "", false) + _ = pairs.Format(formatting) } } @@ -584,3 +697,89 @@ func TestGetPairsByCurrencies(t *testing.T) { t.Fatalf("received %v but expected %v", enabled, 5) } } + +func TestValidateAndConform(t *testing.T) { + t.Parallel() + + conformMe := Pairs{ + NewPair(BTC, USD), + NewPair(LTC, USD), + NewPair(USD, NZD), + NewPair(LTC, USDT), + NewPair(LTC, DAI), + NewPair(USDT, XRP), + NewPair(EMPTYCODE, EMPTYCODE), + } + + _, err := conformMe.ValidateAndConform(EMPTYFORMAT, false) + if !errors.Is(err, ErrCurrencyPairEmpty) { + t.Fatalf("received: '%v' but expected '%v'", err, ErrCurrencyPairEmpty) + } + + duplication, err := NewPairFromString("linkusdt") + if err != nil { + t.Fatal(err) + } + + conformMe = Pairs{ + NewPair(BTC, USD), + NewPair(LTC, USD), + NewPair(LINK, USDT), + NewPair(USD, NZD), + NewPair(LTC, USDT), + NewPair(LTC, DAI), + NewPair(USDT, XRP), + duplication, + } + + _, err = conformMe.ValidateAndConform(EMPTYFORMAT, false) + if !errors.Is(err, ErrPairDuplication) { + t.Fatalf("received: '%v' but expected '%v'", err, ErrPairDuplication) + } + + conformMe = Pairs{ + NewPair(BTC, USD), + NewPair(LTC, USD), + NewPair(LINK, USDT), + NewPair(USD, NZD), + NewPair(LTC, USDT), + NewPair(LTC, DAI), + NewPair(USDT, XRP), + } + + formatted, err := conformMe.ValidateAndConform(EMPTYFORMAT, false) + if !errors.Is(err, nil) { + t.Fatalf("received: '%v' but expected '%v'", err, nil) + } + + expected := "btcusd,ltcusd,linkusdt,usdnzd,ltcusdt,ltcdai,usdtxrp" + + if formatted.Join() != expected { + t.Fatalf("received: '%v' but expected '%v'", formatted.Join(), expected) + } + + formatted, err = formatted.ValidateAndConform(PairFormat{Delimiter: DashDelimiter, Uppercase: true}, false) + if !errors.Is(err, nil) { + t.Fatalf("received: '%v' but expected '%v'", err, nil) + } + + expected = "BTC-USD,LTC-USD,LINK-USDT,USD-NZD,LTC-USDT,LTC-DAI,USDT-XRP" + + if formatted.Join() != expected { + t.Fatalf("received: '%v' but expected '%v'", formatted.Join(), expected) + } + + formatted, err = formatted.ValidateAndConform(PairFormat{ + Delimiter: UnderscoreDelimiter, + Uppercase: false}, + true) + if !errors.Is(err, nil) { + t.Fatalf("received: '%v' but expected '%v'", err, nil) + } + + expected = "BTC-USD,LTC-USD,LINK-USDT,USD-NZD,LTC-USDT,LTC-DAI,USDT-XRP" + + if formatted.Join() != expected { + t.Fatalf("received: '%v' but expected '%v'", formatted.Join(), expected) + } +} diff --git a/engine/engine.go b/engine/engine.go index e15f7c09..0e438f50 100644 --- a/engine/engine.go +++ b/engine/engine.go @@ -792,7 +792,10 @@ func (bot *Engine) LoadExchange(name string, wg *sync.WaitGroup) error { if err != nil { return err } - exchCfg.CurrencyPairs.StorePairs(assets[x], pairs, true) + err = exchCfg.CurrencyPairs.StorePairs(assets[x], pairs, true) + if err != nil { + return err + } } } diff --git a/engine/helpers_test.go b/engine/helpers_test.go index 54de738c..b0c09485 100644 --- a/engine/helpers_test.go +++ b/engine/helpers_test.go @@ -1065,7 +1065,7 @@ func createDepositEngine(opts *fakeDepositExchangeOpts) *Engine { Enabled: true, CurrencyPairs: ¤cy.PairsManager{ UseGlobalFormat: true, - ConfigFormat: ¤cy.PairFormat{}, + ConfigFormat: ¤cy.EMPTYFORMAT, Pairs: map[asset.Item]*currency.PairStore{ asset.Spot: &ps, }, diff --git a/engine/rpcserver.go b/engine/rpcserver.go index 5b59d1fe..1927ca76 100644 --- a/engine/rpcserver.go +++ b/engine/rpcserver.go @@ -76,6 +76,7 @@ var ( errShutdownNotAllowed = errors.New("shutting down this bot instance is not allowed via gRPC, please enable by command line flag --grpcshutdown or config.json field grpcAllowBotShutdown") errGRPCShutdownSignalIsNil = errors.New("cannot shutdown, gRPC shutdown channel is nil") errInvalidStrategy = errors.New("invalid strategy") + errSpecificPairNotEnabled = errors.New("specified pair is not enabled") ) // RPCServer struct @@ -344,14 +345,21 @@ func (s *RPCServer) GetExchangeInfo(_ context.Context, r *gctrpc.GenericExchange resp.SupportedAssets = make(map[string]*gctrpc.PairsSupported) assets := exchCfg.CurrencyPairs.GetAssetTypes(false) for i := range assets { - ps, err := exchCfg.CurrencyPairs.Get(assets[i]) + var enabled currency.Pairs + enabled, err = exchCfg.CurrencyPairs.GetPairs(assets[i], true) + if err != nil { + return nil, err + } + + var available currency.Pairs + available, err = exchCfg.CurrencyPairs.GetPairs(assets[i], false) if err != nil { return nil, err } resp.SupportedAssets[assets[i].String()] = &gctrpc.PairsSupported{ - EnabledPairs: ps.Enabled.Join(), - AvailablePairs: ps.Available.Join(), + EnabledPairs: enabled.Join(), + AvailablePairs: available.Join(), } } return resp, nil @@ -1970,14 +1978,21 @@ func (s *RPCServer) GetExchangePairs(_ context.Context, r *gctrpc.GetExchangePai continue } - ps, err := exchCfg.CurrencyPairs.Get(assetTypes[x]) + var enabled currency.Pairs + enabled, err = exchCfg.CurrencyPairs.GetPairs(assetTypes[x], true) + if err != nil { + return nil, err + } + + var available currency.Pairs + available, err = exchCfg.CurrencyPairs.GetPairs(assetTypes[x], false) if err != nil { return nil, err } resp.SupportedAssets[assetTypes[x].String()] = &gctrpc.PairsSupported{ - AvailablePairs: ps.Available.Join(), - EnabledPairs: ps.Enabled.Join(), + AvailablePairs: available.Join(), + EnabledPairs: enabled.Join(), } } return &resp, nil @@ -2024,31 +2039,36 @@ func (s *RPCServer) SetExchangePair(_ context.Context, r *gctrpc.SetExchangePair } if r.Enable { - err = exchCfg.CurrencyPairs.EnablePair(a, - p.Format(pairFmt.Delimiter, pairFmt.Uppercase)) + err = exchCfg.CurrencyPairs.EnablePair(a, p.Format(pairFmt)) if err != nil { - newErrors = append(newErrors, err) + newErrors = append(newErrors, fmt.Errorf("%s %w", r.Pairs[i], err)) continue } err = base.CurrencyPairs.EnablePair(a, p) if err != nil { - newErrors = append(newErrors, err) + newErrors = append(newErrors, fmt.Errorf("%s %w", r.Pairs[i], err)) continue } pass = true continue } - err = exchCfg.CurrencyPairs.DisablePair(a, - p.Format(pairFmt.Delimiter, pairFmt.Uppercase)) + err = exchCfg.CurrencyPairs.DisablePair(a, p.Format(pairFmt)) if err != nil { - newErrors = append(newErrors, err) - continue + if errors.Is(err, currency.ErrPairNotFound) { + newErrors = append(newErrors, fmt.Errorf("%s %w", r.Pairs[i], errSpecificPairNotEnabled)) + continue + } + return nil, err } + err = base.CurrencyPairs.DisablePair(a, p) if err != nil { - newErrors = append(newErrors, err) - continue + if errors.Is(err, currency.ErrPairNotFound) { + newErrors = append(newErrors, fmt.Errorf("%s %w", r.Pairs[i], errSpecificPairNotEnabled)) + continue + } + return nil, err } pass = true } @@ -2913,13 +2933,25 @@ func (s *RPCServer) SetAllExchangePairs(_ context.Context, r *gctrpc.SetExchange if err != nil { return nil, err } - exchCfg.CurrencyPairs.StorePairs(assets[i], pairs, true) - base.CurrencyPairs.StorePairs(assets[i], pairs, true) + err = exchCfg.CurrencyPairs.StorePairs(assets[i], pairs, true) + if err != nil { + return nil, err + } + err = base.CurrencyPairs.StorePairs(assets[i], pairs, true) + if err != nil { + return nil, err + } } } else { for i := range assets { - exchCfg.CurrencyPairs.StorePairs(assets[i], nil, true) - base.CurrencyPairs.StorePairs(assets[i], nil, true) + err = exchCfg.CurrencyPairs.StorePairs(assets[i], nil, true) + if err != nil { + return nil, err + } + err = base.CurrencyPairs.StorePairs(assets[i], nil, true) + if err != nil { + return nil, err + } } } diff --git a/engine/rpcserver_test.go b/engine/rpcserver_test.go index 1bc4c9d4..1c564d19 100644 --- a/engine/rpcserver_test.go +++ b/engine/rpcserver_test.go @@ -1464,6 +1464,7 @@ func TestCheckVars(t *testing.T) { }, ConfigFormat: ¤cy.PairFormat{ Uppercase: true, + Delimiter: currency.DashDelimiter, }, } err = e.GetBase().StoreAssetPairFormat(asset.Spot, fmt1) @@ -1492,7 +1493,10 @@ func TestCheckVars(t *testing.T) { {Delimiter: currency.DashDelimiter, Base: currency.BTC, Quote: currency.USDT}, } - e.GetBase().CurrencyPairs.StorePairs(asset.Spot, data, false) + err = e.GetBase().CurrencyPairs.StorePairs(asset.Spot, data, false) + if err != nil { + t.Fatal(err) + } err = checkParams("Binance", e, asset.Spot, currency.NewPair(currency.BTC, currency.USDT)) if !errors.Is(err, errCurrencyNotEnabled) { @@ -2174,7 +2178,7 @@ func TestCurrencyStateTradingPair(t *testing.T) { b.CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore) b.CurrencyPairs.Pairs[asset.Spot] = ¤cy.PairStore{ AssetEnabled: convert.BoolPtr(true), - ConfigFormat: ¤cy.PairFormat{}, + ConfigFormat: ¤cy.EMPTYFORMAT, Available: currency.Pairs{cp}, Enabled: currency.Pairs{cp}, } @@ -2351,13 +2355,13 @@ func TestGetCollateral(t *testing.T) { b.CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore) b.CurrencyPairs.Pairs[asset.Futures] = ¤cy.PairStore{ AssetEnabled: convert.BoolPtr(true), - ConfigFormat: ¤cy.PairFormat{}, + ConfigFormat: ¤cy.EMPTYFORMAT, Available: currency.Pairs{cp}, Enabled: currency.Pairs{cp}, } b.CurrencyPairs.Pairs[asset.Spot] = ¤cy.PairStore{ AssetEnabled: convert.BoolPtr(true), - ConfigFormat: ¤cy.PairFormat{}, + ConfigFormat: ¤cy.EMPTYFORMAT, Available: currency.Pairs{cp}, Enabled: currency.Pairs{cp}, } diff --git a/engine/sync_manager.go b/engine/sync_manager.go index 2d0e363f..e4d74d39 100644 --- a/engine/sync_manager.go +++ b/engine/sync_manager.go @@ -87,8 +87,7 @@ func setupSyncManager(c *SyncManagerConfig, exchangeManager iExchangeManager, re exchangeManager: exchangeManager, websocketRoutineManagerEnabled: websocketRoutineManagerEnabled, fiatDisplayCurrency: c.FiatDisplayCurrency, - delimiter: c.PairFormatDisplay.Delimiter, - uppercase: c.PairFormatDisplay.Uppercase, + format: *c.PairFormatDisplay, tickerBatchLastRequested: make(map[string]time.Time), } @@ -813,7 +812,7 @@ func (m *syncManager) FormatCurrency(p currency.Pair) currency.Pair { if m == nil || atomic.LoadInt32(&m.started) == 0 { return p } - return p.Format(m.delimiter, m.uppercase) + return p.Format(m.format) } const ( diff --git a/engine/sync_manager_test.go b/engine/sync_manager_test.go index fe8a9a90..153dcb6a 100644 --- a/engine/sync_manager_test.go +++ b/engine/sync_manager_test.go @@ -49,7 +49,7 @@ func TestSetupSyncManager(t *testing.T) { t.Errorf("error '%v', expected '%v'", err, common.ErrNilPointer) } - m, err := setupSyncManager(&SyncManagerConfig{SynchronizeTrades: true, FiatDisplayCurrency: currency.USD, PairFormatDisplay: ¤cy.PairFormat{}}, &ExchangeManager{}, &config.RemoteControlConfig{}, true) + m, err := setupSyncManager(&SyncManagerConfig{SynchronizeTrades: true, FiatDisplayCurrency: currency.USD, PairFormatDisplay: ¤cy.EMPTYFORMAT}, &ExchangeManager{}, &config.RemoteControlConfig{}, true) if !errors.Is(err, nil) { t.Errorf("error '%v', expected '%v'", err, nil) } @@ -60,7 +60,7 @@ func TestSetupSyncManager(t *testing.T) { func TestSyncManagerStart(t *testing.T) { t.Parallel() - m, err := setupSyncManager(&SyncManagerConfig{SynchronizeTrades: true, FiatDisplayCurrency: currency.USD, PairFormatDisplay: ¤cy.PairFormat{}}, &ExchangeManager{}, &config.RemoteControlConfig{}, true) + m, err := setupSyncManager(&SyncManagerConfig{SynchronizeTrades: true, FiatDisplayCurrency: currency.USD, PairFormatDisplay: ¤cy.EMPTYFORMAT}, &ExchangeManager{}, &config.RemoteControlConfig{}, true) if !errors.Is(err, nil) { t.Errorf("error '%v', expected '%v'", err, nil) } @@ -105,7 +105,7 @@ func TestSyncManagerStop(t *testing.T) { } exch.SetDefaults() em.Add(exch) - m, err = setupSyncManager(&SyncManagerConfig{SynchronizeTrades: true, SynchronizeContinuously: true, FiatDisplayCurrency: currency.USD, PairFormatDisplay: ¤cy.PairFormat{}}, em, &config.RemoteControlConfig{}, false) + m, err = setupSyncManager(&SyncManagerConfig{SynchronizeTrades: true, SynchronizeContinuously: true, FiatDisplayCurrency: currency.USD, PairFormatDisplay: ¤cy.EMPTYFORMAT}, em, &config.RemoteControlConfig{}, false) if !errors.Is(err, nil) { t.Errorf("error '%v', expected '%v'", err, nil) } @@ -153,7 +153,7 @@ func TestPrintTickerSummary(t *testing.T) { } exch.SetDefaults() em.Add(exch) - m, err = setupSyncManager(&SyncManagerConfig{SynchronizeTrades: true, SynchronizeContinuously: true, FiatDisplayCurrency: currency.USD, PairFormatDisplay: ¤cy.PairFormat{}}, em, &config.RemoteControlConfig{}, false) + m, err = setupSyncManager(&SyncManagerConfig{SynchronizeTrades: true, SynchronizeContinuously: true, FiatDisplayCurrency: currency.USD, PairFormatDisplay: ¤cy.EMPTYFORMAT}, em, &config.RemoteControlConfig{}, false) if !errors.Is(err, nil) { t.Errorf("error '%v', expected '%v'", err, nil) } @@ -192,7 +192,7 @@ func TestPrintOrderbookSummary(t *testing.T) { } exch.SetDefaults() em.Add(exch) - m, err = setupSyncManager(&SyncManagerConfig{SynchronizeTrades: true, SynchronizeContinuously: true, FiatDisplayCurrency: currency.USD, PairFormatDisplay: ¤cy.PairFormat{}}, em, &config.RemoteControlConfig{}, false) + m, err = setupSyncManager(&SyncManagerConfig{SynchronizeTrades: true, SynchronizeContinuously: true, FiatDisplayCurrency: currency.USD, PairFormatDisplay: ¤cy.EMPTYFORMAT}, em, &config.RemoteControlConfig{}, false) if !errors.Is(err, nil) { t.Errorf("error '%v', expected '%v'", err, nil) } diff --git a/engine/sync_manager_types.go b/engine/sync_manager_types.go index e0656e04..7f422f7a 100644 --- a/engine/sync_manager_types.go +++ b/engine/sync_manager_types.go @@ -49,8 +49,7 @@ type syncManager struct { initSyncCompleted int32 initSyncStarted int32 started int32 - delimiter string - uppercase bool + format currency.PairFormat initSyncStartTime time.Time fiatDisplayCurrency currency.Code websocketRoutineManagerEnabled bool diff --git a/engine/websocketroutine_manager.go b/engine/websocketroutine_manager.go index 735e8693..e74b69c7 100644 --- a/engine/websocketroutine_manager.go +++ b/engine/websocketroutine_manager.go @@ -30,7 +30,7 @@ func setupWebsocketRoutineManager(exchangeManager iExchangeManager, orderManager if cfg == nil { return nil, errNilCurrencyConfig } - if cfg.CurrencyPairFormat == nil && verbose { + if cfg.CurrencyPairFormat == nil { return nil, errNilCurrencyPairFormat } man := &websocketRoutineManager{ @@ -49,6 +49,15 @@ func (m *websocketRoutineManager) Start() error { if m == nil { return fmt.Errorf("websocket routine manager %w", ErrNilSubsystem) } + + if m.currencyConfig == nil { + return errNilCurrencyConfig + } + + if m.currencyConfig.CurrencyPairFormat == nil { + return errNilCurrencyPairFormat + } + if !atomic.CompareAndSwapInt32(&m.started, 0, 1) { return ErrSubSystemAlreadyStarted } @@ -288,8 +297,7 @@ func (m *websocketRoutineManager) FormatCurrency(p currency.Pair) currency.Pair if m == nil || atomic.LoadInt32(&m.started) == 0 { return p } - return p.Format(m.currencyConfig.CurrencyPairFormat.Delimiter, - m.currencyConfig.CurrencyPairFormat.Uppercase) + return p.Format(*m.currencyConfig.CurrencyPairFormat) } // printOrderSummary this function will be deprecated when a order manager diff --git a/engine/websocketroutine_manager_test.go b/engine/websocketroutine_manager_test.go index b23a6f5c..5d9df8e6 100644 --- a/engine/websocketroutine_manager_test.go +++ b/engine/websocketroutine_manager_test.go @@ -38,7 +38,7 @@ func TestWebsocketRoutineManagerSetup(t *testing.T) { t.Errorf("error '%v', expected '%v'", err, errNilCurrencyPairFormat) } - m, err := setupWebsocketRoutineManager(SetupExchangeManager(), &OrderManager{}, &syncManager{}, ¤cy.Config{}, false) + m, err := setupWebsocketRoutineManager(SetupExchangeManager(), &OrderManager{}, &syncManager{}, ¤cy.Config{CurrencyPairFormat: ¤cy.PairFormat{}}, false) if !errors.Is(err, nil) { t.Errorf("error '%v', expected '%v'", err, nil) } @@ -77,7 +77,7 @@ func TestWebsocketRoutineManagerIsRunning(t *testing.T) { t.Error("expected false") } - m, err := setupWebsocketRoutineManager(SetupExchangeManager(), &OrderManager{}, &syncManager{}, ¤cy.Config{}, false) + m, err := setupWebsocketRoutineManager(SetupExchangeManager(), &OrderManager{}, &syncManager{}, ¤cy.Config{CurrencyPairFormat: ¤cy.PairFormat{}}, false) if !errors.Is(err, nil) { t.Errorf("error '%v', expected '%v'", err, nil) } @@ -101,7 +101,7 @@ func TestWebsocketRoutineManagerStop(t *testing.T) { t.Errorf("error '%v', expected '%v'", err, ErrNilSubsystem) } - m, err = setupWebsocketRoutineManager(SetupExchangeManager(), &OrderManager{}, &syncManager{}, ¤cy.Config{}, false) + m, err = setupWebsocketRoutineManager(SetupExchangeManager(), &OrderManager{}, &syncManager{}, ¤cy.Config{CurrencyPairFormat: ¤cy.PairFormat{}}, false) if !errors.Is(err, nil) { t.Errorf("error '%v', expected '%v'", err, nil) } diff --git a/exchanges/binance/binance_wrapper.go b/exchanges/binance/binance_wrapper.go index e212f4f1..dfedbe9d 100644 --- a/exchanges/binance/binance_wrapper.go +++ b/exchanges/binance/binance_wrapper.go @@ -1893,7 +1893,7 @@ func (b *Binance) FormatExchangeCurrency(p currency.Pair, a asset.Item) (currenc if a == asset.USDTMarginedFutures { return b.formatUSDTMarginedFuturesPair(p, pairFmt), nil } - return p.Format(pairFmt.Delimiter, pairFmt.Uppercase), nil + return p.Format(pairFmt), nil } // FormatSymbol formats the given pair to a string suitable for exchange API requests @@ -1917,10 +1917,11 @@ func (b *Binance) formatUSDTMarginedFuturesPair(p currency.Pair, pairFmt currenc for _, c := range quote { if c < '0' || c > '9' { // character rune is alphabetic, cannot be expiring contract - return p.Format(pairFmt.Delimiter, pairFmt.Uppercase) + return p.Format(pairFmt) } } - return p.Format(currency.UnderscoreDelimiter, pairFmt.Uppercase) + pairFmt.Delimiter = currency.UnderscoreDelimiter + return p.Format(pairFmt) } // GetServerTime returns the current exchange server time. diff --git a/exchanges/bitfinex/bitfinex_wrapper.go b/exchanges/bitfinex/bitfinex_wrapper.go index 0e6fe7aa..762687c6 100644 --- a/exchanges/bitfinex/bitfinex_wrapper.go +++ b/exchanges/bitfinex/bitfinex_wrapper.go @@ -65,7 +65,7 @@ func (b *Bitfinex) SetDefaults() { fmt1 := currency.PairStore{ RequestFormat: ¤cy.PairFormat{Uppercase: true}, - ConfigFormat: ¤cy.PairFormat{Uppercase: true}, + ConfigFormat: ¤cy.PairFormat{Uppercase: true, Delimiter: currency.DashDelimiter}, } fmt2 := currency.PairStore{ @@ -323,9 +323,20 @@ func (b *Bitfinex) UpdateTradablePairs(ctx context.Context, forceUpdate bool) er return err } - p, err := currency.NewPairsFromStrings(pairs) - if err != nil { - return err + var p currency.Pairs + if assets[i] == asset.MarginFunding { + p = make(currency.Pairs, len(pairs)) + for x := range pairs { + p[x], err = currency.NewPairFromStrings(pairs[x], "") + if err != nil { + return err + } + } + } else { + p, err = currency.NewPairsFromStrings(pairs) + if err != nil { + return err + } } err = b.UpdatePairs(p, assets[i], false, forceUpdate) diff --git a/exchanges/bithumb/bithumb_test.go b/exchanges/bithumb/bithumb_test.go index 48b3de8d..61370ecd 100644 --- a/exchanges/bithumb/bithumb_test.go +++ b/exchanges/bithumb/bithumb_test.go @@ -51,6 +51,11 @@ func TestMain(m *testing.M) { log.Fatal("Bithumb setup error", err) } + err = b.UpdateTradablePairs(context.Background(), false) + if err != nil { + log.Fatal("Bithumb Setup() init error") + } + os.Exit(m.Run()) } diff --git a/exchanges/bithumb/bithumb_websocket.go b/exchanges/bithumb/bithumb_websocket.go index c4994d61..0d7b4406 100644 --- a/exchanges/bithumb/bithumb_websocket.go +++ b/exchanges/bithumb/bithumb_websocket.go @@ -175,11 +175,16 @@ func (b *Bithumb) GenerateSubscriptions() ([]stream.ChannelSubscription, error) return nil, err } + pFmt, err := b.GetPairFormat(asset.Spot, true) + if err != nil { + return nil, err + } + for x := range pairs { for y := range channels { subscriptions = append(subscriptions, stream.ChannelSubscription{ Channel: channels[y], - Currency: pairs[x].Format("_", true), + Currency: pairs[x].Format(pFmt), Asset: asset.Spot, }) } diff --git a/exchanges/bithumb/bithumb_websocket_test.go b/exchanges/bithumb/bithumb_websocket_test.go index d0f41165..f5eeabf9 100644 --- a/exchanges/bithumb/bithumb_websocket_test.go +++ b/exchanges/bithumb/bithumb_websocket_test.go @@ -40,6 +40,7 @@ func TestWsHandleData(t *testing.T) { Enabled: pairs, ConfigFormat: ¤cy.PairFormat{ Uppercase: true, + Delimiter: currency.DashDelimiter, }, }, }, diff --git a/exchanges/bithumb/bithumb_wrapper.go b/exchanges/bithumb/bithumb_wrapper.go index ffd45711..5715b645 100644 --- a/exchanges/bithumb/bithumb_wrapper.go +++ b/exchanges/bithumb/bithumb_wrapper.go @@ -67,7 +67,7 @@ func (b *Bithumb) SetDefaults() { b.API.CredentialsValidator.RequiresSecret = true requestFmt := ¤cy.PairFormat{Uppercase: true, Delimiter: currency.UnderscoreDelimiter} - configFmt := ¤cy.PairFormat{Uppercase: true, Index: "KRW"} + configFmt := ¤cy.PairFormat{Uppercase: true, Delimiter: currency.DashDelimiter} err := b.SetGlobalPairsManager(requestFmt, configFmt, asset.Spot) if err != nil { log.Errorln(log.ExchangeSys, err) @@ -237,7 +237,7 @@ func (b *Bithumb) FetchTradablePairs(ctx context.Context, asset asset.Item) ([]s } for x := range currencies { - currencies[x] += "KRW" + currencies[x] += currency.DashDelimiter + "KRW" } return currencies, nil diff --git a/exchanges/bitmex/bitmex.go b/exchanges/bitmex/bitmex.go index 9943989b..8028d66c 100644 --- a/exchanges/bitmex/bitmex.go +++ b/exchanges/bitmex/bitmex.go @@ -103,6 +103,14 @@ const ( ContractDownsideProfit // ContractUpsideProfit upside profit contract type ContractUpsideProfit + + perpetualContractID = "FFWCSX" + spotID = "IFXXXP" + futuresID = "FFCCSX" + bitMEXBasketIndexID = "MRBXXX" + bitMEXPriceIndexID = "MRCXXX" + bitMEXLendingPremiumIndexID = "MRRXXX" + bitMEXVolatilityIndexID = "MRIXXX" ) // GetAnnouncement returns the general announcements from Bitmex diff --git a/exchanges/bitmex/bitmex_types.go b/exchanges/bitmex/bitmex_types.go index 33e8a95a..060ac842 100644 --- a/exchanges/bitmex/bitmex_types.go +++ b/exchanges/bitmex/bitmex_types.go @@ -3,7 +3,6 @@ package bitmex import ( "time" - "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/exchanges/order" ) @@ -122,107 +121,107 @@ type Funding struct { // Instrument Tradeable Contracts, Indices, and History type Instrument struct { - AskPrice float64 `json:"askPrice"` - BankruptLimitDownPrice float64 `json:"bankruptLimitDownPrice"` - BankruptLimitUpPrice float64 `json:"bankruptLimitUpPrice"` - BidPrice float64 `json:"bidPrice"` - BuyLeg string `json:"buyLeg"` - CalcInterval string `json:"calcInterval"` - Capped bool `json:"capped"` - ClosingTimestamp time.Time `json:"closingTimestamp"` - Deleverage bool `json:"deleverage"` - Expiry string `json:"expiry"` - FairBasis float64 `json:"fairBasis"` - FairBasisRate float64 `json:"fairBasisRate"` - FairMethod string `json:"fairMethod"` - FairPrice float64 `json:"fairPrice"` - Front string `json:"front"` - FundingBaseSymbol string `json:"fundingBaseSymbol"` - FundingInterval string `json:"fundingInterval"` - FundingPremiumSymbol string `json:"fundingPremiumSymbol"` - FundingQuoteSymbol string `json:"fundingQuoteSymbol"` - FundingRate float64 `json:"fundingRate"` - FundingTimestamp time.Time `json:"fundingTimestamp"` - HasLiquidity bool `json:"hasLiquidity"` - HighPrice float64 `json:"highPrice"` - ImpactAskPrice float64 `json:"impactAskPrice"` - ImpactBidPrice float64 `json:"impactBidPrice"` - ImpactMidPrice float64 `json:"impactMidPrice"` - IndicativeFundingRate float64 `json:"indicativeFundingRate"` - IndicativeSettlePrice float64 `json:"indicativeSettlePrice"` - IndicativeTaxRate float64 `json:"indicativeTaxRate"` - InitMargin float64 `json:"initMargin"` - InsuranceFee float64 `json:"insuranceFee"` - InverseLeg string `json:"inverseLeg"` - IsInverse bool `json:"isInverse"` - IsQuanto bool `json:"isQuanto"` - LastChangePcnt float64 `json:"lastChangePcnt"` - LastPrice float64 `json:"lastPrice"` - LastPriceProtected float64 `json:"lastPriceProtected"` - LastTickDirection string `json:"lastTickDirection"` - Limit float64 `json:"limit"` - LimitDownPrice float64 `json:"limitDownPrice"` - LimitUpPrice float64 `json:"limitUpPrice"` - Listing string `json:"listing"` - LotSize int64 `json:"lotSize"` - LowPrice float64 `json:"lowPrice"` - MaintMargin float64 `json:"maintMargin"` - MakerFee float64 `json:"makerFee"` - MarkMethod string `json:"markMethod"` - MarkPrice float64 `json:"markPrice"` - MaxOrderQty int64 `json:"maxOrderQty"` - MaxPrice float64 `json:"maxPrice"` - MidPrice float64 `json:"midPrice"` - Multiplier int64 `json:"multiplier"` - OpenInterest int64 `json:"openInterest"` - OpenValue int64 `json:"openValue"` - OpeningTimestamp time.Time `json:"openingTimestamp"` - OptionMultiplier float64 `json:"optionMultiplier"` - OptionStrikePcnt float64 `json:"optionStrikePcnt"` - OptionStrikePrice float64 `json:"optionStrikePrice"` - OptionStrikeRound float64 `json:"optionStrikeRound"` - OptionUnderlyingPrice float64 `json:"optionUnderlyingPrice"` - PositionCurrency string `json:"positionCurrency"` - PrevClosePrice float64 `json:"prevClosePrice"` - PrevPrice24h float64 `json:"prevPrice24h"` - PrevTotalTurnover int64 `json:"prevTotalTurnover"` - PrevTotalVolume int64 `json:"prevTotalVolume"` - PublishInterval string `json:"publishInterval"` - PublishTime string `json:"publishTime"` - QuoteCurrency string `json:"quoteCurrency"` - QuoteToSettleMultiplier int64 `json:"quoteToSettleMultiplier"` - RebalanceInterval string `json:"rebalanceInterval"` - RebalanceTimestamp time.Time `json:"rebalanceTimestamp"` - Reference string `json:"reference"` - ReferenceSymbol string `json:"referenceSymbol"` - RelistInterval string `json:"relistInterval"` - RiskLimit int64 `json:"riskLimit"` - RiskStep int64 `json:"riskStep"` - RootSymbol string `json:"rootSymbol"` - SellLeg string `json:"sellLeg"` - SessionInterval string `json:"sessionInterval"` - SettlCurrency string `json:"settlCurrency"` - Settle string `json:"settle"` - SettledPrice float64 `json:"settledPrice"` - SettlementFee float64 `json:"settlementFee"` - State string `json:"state"` - Symbol currency.Pair `json:"symbol"` - TakerFee float64 `json:"takerFee"` - Taxed bool `json:"taxed"` - TickSize float64 `json:"tickSize"` - Timestamp time.Time `json:"timestamp"` - TotalTurnover int64 `json:"totalTurnover"` - TotalVolume int64 `json:"totalVolume"` - Turnover int64 `json:"turnover"` - Turnover24h int64 `json:"turnover24h"` - Typ string `json:"typ"` - Underlying string `json:"underlying"` - UnderlyingSymbol string `json:"underlyingSymbol"` - UnderlyingToPositionMultiplier int64 `json:"underlyingToPositionMultiplier"` - UnderlyingToSettleMultiplier int64 `json:"underlyingToSettleMultiplier"` - Volume float64 `json:"volume"` - Volume24h float64 `json:"volume24h"` - Vwap float64 `json:"vwap"` + AskPrice float64 `json:"askPrice"` + BankruptLimitDownPrice float64 `json:"bankruptLimitDownPrice"` + BankruptLimitUpPrice float64 `json:"bankruptLimitUpPrice"` + BidPrice float64 `json:"bidPrice"` + BuyLeg string `json:"buyLeg"` + CalcInterval string `json:"calcInterval"` + Capped bool `json:"capped"` + ClosingTimestamp time.Time `json:"closingTimestamp"` + Deleverage bool `json:"deleverage"` + Expiry string `json:"expiry"` + FairBasis float64 `json:"fairBasis"` + FairBasisRate float64 `json:"fairBasisRate"` + FairMethod string `json:"fairMethod"` + FairPrice float64 `json:"fairPrice"` + Front string `json:"front"` + FundingBaseSymbol string `json:"fundingBaseSymbol"` + FundingInterval string `json:"fundingInterval"` + FundingPremiumSymbol string `json:"fundingPremiumSymbol"` + FundingQuoteSymbol string `json:"fundingQuoteSymbol"` + FundingRate float64 `json:"fundingRate"` + FundingTimestamp time.Time `json:"fundingTimestamp"` + HasLiquidity bool `json:"hasLiquidity"` + HighPrice float64 `json:"highPrice"` + ImpactAskPrice float64 `json:"impactAskPrice"` + ImpactBidPrice float64 `json:"impactBidPrice"` + ImpactMidPrice float64 `json:"impactMidPrice"` + IndicativeFundingRate float64 `json:"indicativeFundingRate"` + IndicativeSettlePrice float64 `json:"indicativeSettlePrice"` + IndicativeTaxRate float64 `json:"indicativeTaxRate"` + InitMargin float64 `json:"initMargin"` + InsuranceFee float64 `json:"insuranceFee"` + InverseLeg string `json:"inverseLeg"` + IsInverse bool `json:"isInverse"` + IsQuanto bool `json:"isQuanto"` + LastChangePcnt float64 `json:"lastChangePcnt"` + LastPrice float64 `json:"lastPrice"` + LastPriceProtected float64 `json:"lastPriceProtected"` + LastTickDirection string `json:"lastTickDirection"` + Limit float64 `json:"limit"` + LimitDownPrice float64 `json:"limitDownPrice"` + LimitUpPrice float64 `json:"limitUpPrice"` + Listing string `json:"listing"` + LotSize int64 `json:"lotSize"` + LowPrice float64 `json:"lowPrice"` + MaintMargin float64 `json:"maintMargin"` + MakerFee float64 `json:"makerFee"` + MarkMethod string `json:"markMethod"` + MarkPrice float64 `json:"markPrice"` + MaxOrderQty int64 `json:"maxOrderQty"` + MaxPrice float64 `json:"maxPrice"` + MidPrice float64 `json:"midPrice"` + Multiplier int64 `json:"multiplier"` + OpenInterest int64 `json:"openInterest"` + OpenValue int64 `json:"openValue"` + OpeningTimestamp time.Time `json:"openingTimestamp"` + OptionMultiplier float64 `json:"optionMultiplier"` + OptionStrikePcnt float64 `json:"optionStrikePcnt"` + OptionStrikePrice float64 `json:"optionStrikePrice"` + OptionStrikeRound float64 `json:"optionStrikeRound"` + OptionUnderlyingPrice float64 `json:"optionUnderlyingPrice"` + PositionCurrency string `json:"positionCurrency"` + PrevClosePrice float64 `json:"prevClosePrice"` + PrevPrice24h float64 `json:"prevPrice24h"` + PrevTotalTurnover int64 `json:"prevTotalTurnover"` + PrevTotalVolume int64 `json:"prevTotalVolume"` + PublishInterval string `json:"publishInterval"` + PublishTime string `json:"publishTime"` + QuoteCurrency string `json:"quoteCurrency"` + QuoteToSettleMultiplier int64 `json:"quoteToSettleMultiplier"` + RebalanceInterval string `json:"rebalanceInterval"` + RebalanceTimestamp time.Time `json:"rebalanceTimestamp"` + Reference string `json:"reference"` + ReferenceSymbol string `json:"referenceSymbol"` + RelistInterval string `json:"relistInterval"` + RiskLimit int64 `json:"riskLimit"` + RiskStep int64 `json:"riskStep"` + RootSymbol string `json:"rootSymbol"` + SellLeg string `json:"sellLeg"` + SessionInterval string `json:"sessionInterval"` + SettlCurrency string `json:"settlCurrency"` + Settle string `json:"settle"` + SettledPrice float64 `json:"settledPrice"` + SettlementFee float64 `json:"settlementFee"` + State string `json:"state"` + Symbol string `json:"symbol"` + TakerFee float64 `json:"takerFee"` + Taxed bool `json:"taxed"` + TickSize float64 `json:"tickSize"` + Timestamp time.Time `json:"timestamp"` + TotalTurnover int64 `json:"totalTurnover"` + TotalVolume int64 `json:"totalVolume"` + Turnover int64 `json:"turnover"` + Turnover24h int64 `json:"turnover24h"` + Typ string `json:"typ"` + Underlying string `json:"underlying"` + UnderlyingSymbol string `json:"underlyingSymbol"` + UnderlyingToPositionMultiplier int64 `json:"underlyingToPositionMultiplier"` + UnderlyingToSettleMultiplier int64 `json:"underlyingToSettleMultiplier"` + Volume float64 `json:"volume"` + Volume24h float64 `json:"volume24h"` + Vwap float64 `json:"vwap"` } // InstrumentInterval instrument interval diff --git a/exchanges/bitmex/bitmex_websocket.go b/exchanges/bitmex/bitmex_websocket.go index 6e8efa66..d7e70190 100644 --- a/exchanges/bitmex/bitmex_websocket.go +++ b/exchanges/bitmex/bitmex_websocket.go @@ -186,26 +186,18 @@ func (b *Bitmex) wsHandleData(respRaw []byte) error { if len(orderbooks.Data) == 0 { return fmt.Errorf("%s - Empty orderbook data received: %s", b.Name, respRaw) } - var p currency.Pair - p, err = currency.NewPairFromString(orderbooks.Data[0].Symbol) - if err != nil { - return err - } + var pair currency.Pair var a asset.Item - a, err = b.GetPairAssetType(p) + pair, a, err = b.GetPairAndAssetTypeRequestFormatted(orderbooks.Data[0].Symbol) if err != nil { return err } - err = b.processOrderbook(orderbooks.Data, - orderbooks.Action, - p, - a) + err = b.processOrderbook(orderbooks.Data, orderbooks.Action, pair, a) if err != nil { return err } - case bitmexWSTrade: if !b.IsSaveTradeDataEnabled() { return nil @@ -223,13 +215,8 @@ func (b *Bitmex) wsHandleData(respRaw []byte) error { continue } var p currency.Pair - p, err = currency.NewPairFromString(tradeHolder.Data[i].Symbol) - if err != nil { - return err - } - var a asset.Item - a, err = b.GetPairAssetType(p) + p, a, err = b.GetPairAndAssetTypeRequestFormatted(tradeHolder.Data[i].Symbol) if err != nil { return err } @@ -285,13 +272,8 @@ func (b *Bitmex) wsHandleData(respRaw []byte) error { for i := range response.Data { var p currency.Pair - p, err = currency.NewPairFromString(response.Data[i].Symbol) - if err != nil { - return err - } - var a asset.Item - a, err = b.GetPairAssetType(p) + p, a, err = b.GetPairAndAssetTypeRequestFormatted(response.Data[i].Symbol) if err != nil { return err } @@ -570,6 +552,10 @@ func (b *Bitmex) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, e assets := b.GetAssetTypes(true) for x := range assets { + pFmt, err := b.GetPairFormat(assets[x], true) + if err != nil { + return nil, err + } contracts, err := b.GetEnabledPairs(assets[x]) if err != nil { return nil, err @@ -581,7 +567,7 @@ func (b *Bitmex) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, e continue } subscriptions = append(subscriptions, stream.ChannelSubscription{ - Channel: channels[z] + ":" + contracts[y].String(), + Channel: channels[z] + ":" + pFmt.Format(contracts[y]), Currency: contracts[y], Asset: assets[x], }) @@ -596,6 +582,11 @@ func (b *Bitmex) GenerateAuthenticatedSubscriptions() ([]stream.ChannelSubscript if !b.Websocket.CanUseAuthenticatedEndpoints() { return nil, nil } + + pFmt, err := b.GetPairFormat(asset.PerpetualContract, true) + if err != nil { + return nil, err + } contracts, err := b.GetEnabledPairs(asset.PerpetualContract) if err != nil { return nil, err @@ -626,7 +617,7 @@ func (b *Bitmex) GenerateAuthenticatedSubscriptions() ([]stream.ChannelSubscript for i := range channels { for j := range contracts { subscriptions = append(subscriptions, stream.ChannelSubscription{ - Channel: channels[i] + ":" + contracts[j].String(), + Channel: channels[i] + ":" + pFmt.Format(contracts[j]), Currency: contracts[j], Asset: asset.PerpetualContract, }) @@ -639,7 +630,6 @@ func (b *Bitmex) GenerateAuthenticatedSubscriptions() ([]stream.ChannelSubscript func (b *Bitmex) Subscribe(channelsToSubscribe []stream.ChannelSubscription) error { var subscriber WebsocketRequest subscriber.Command = "subscribe" - for i := range channelsToSubscribe { subscriber.Arguments = append(subscriber.Arguments, channelsToSubscribe[i].Channel) diff --git a/exchanges/bitmex/bitmex_wrapper.go b/exchanges/bitmex/bitmex_wrapper.go index 590533c4..b2fd7b25 100644 --- a/exchanges/bitmex/bitmex_wrapper.go +++ b/exchanges/bitmex/bitmex_wrapper.go @@ -62,13 +62,30 @@ func (b *Bitmex) SetDefaults() { b.API.CredentialsValidator.RequiresKey = true b.API.CredentialsValidator.RequiresSecret = true - requestFmt := ¤cy.PairFormat{Uppercase: true} - configFmt := ¤cy.PairFormat{Uppercase: true} - err := b.SetGlobalPairsManager(requestFmt, - configFmt, - asset.PerpetualContract, - asset.Futures, - asset.Index) + configFmt := ¤cy.PairFormat{Uppercase: true, Delimiter: currency.DashDelimiter} + standardRequestFmt := ¤cy.PairFormat{Uppercase: true} + spotRequestFormat := ¤cy.PairFormat{Uppercase: true, Delimiter: currency.UnderscoreDelimiter} + + spot := currency.PairStore{RequestFormat: spotRequestFormat, ConfigFormat: configFmt} + err := b.StoreAssetPairFormat(asset.Spot, spot) + if err != nil { + log.Errorln(log.ExchangeSys, err) + } + + perp := currency.PairStore{RequestFormat: standardRequestFmt, ConfigFormat: configFmt} + err = b.StoreAssetPairFormat(asset.PerpetualContract, perp) + if err != nil { + log.Errorln(log.ExchangeSys, err) + } + + futures := currency.PairStore{RequestFormat: standardRequestFmt, ConfigFormat: configFmt} + err = b.StoreAssetPairFormat(asset.Futures, futures) + if err != nil { + log.Errorln(log.ExchangeSys, err) + } + + index := currency.PairStore{RequestFormat: standardRequestFmt, ConfigFormat: configFmt} + err = b.StoreAssetPairFormat(asset.Index, index) if err != nil { log.Errorln(log.ExchangeSys, err) } @@ -227,66 +244,99 @@ func (b *Bitmex) Run() { } // FetchTradablePairs returns a list of the exchanges tradable pairs -func (b *Bitmex) FetchTradablePairs(ctx context.Context, asset asset.Item) ([]string, error) { +func (b *Bitmex) FetchTradablePairs(ctx context.Context, a asset.Item) ([]string, error) { marketInfo, err := b.GetActiveAndIndexInstruments(ctx) if err != nil { return nil, err } - products := make([]string, len(marketInfo)) + products := make([]string, 0, len(marketInfo)) for x := range marketInfo { - products[x] = marketInfo[x].Symbol.String() - } + if marketInfo[x].State != "Open" && a != asset.Index { + continue + } + switch a { + case asset.Spot: + if marketInfo[x].Typ == spotID { + products = append(products, marketInfo[x].Symbol) + } + case asset.PerpetualContract: + if marketInfo[x].Typ == perpetualContractID { + var settleTrail string + if strings.Contains(marketInfo[x].Symbol, currency.UnderscoreDelimiter) { + // Example: ETHUSD_ETH quoted in USD, paid out in ETH. + settlement := strings.Split(marketInfo[x].Symbol, currency.UnderscoreDelimiter) + if len(settlement) != 2 { + log.Warnf(log.ExchangeSys, "%s currency %s %s cannot be added to tradable pairs", + b.Name, + marketInfo[x].Symbol, + a) + break + } + settleTrail = currency.UnderscoreDelimiter + settlement[1] + } + products = append(products, marketInfo[x].Underlying+ + currency.DashDelimiter+ + marketInfo[x].QuoteCurrency+settleTrail) + } + case asset.Futures: + if marketInfo[x].Typ == futuresID { + isolate := strings.Split(marketInfo[x].Symbol, currency.UnderscoreDelimiter) + if len(isolate[0]) < 3 { + log.Warnf(log.ExchangeSys, "%s currency %s %s be cannot added to tradable pairs", + b.Name, + marketInfo[x].Symbol, + a) + break + } + var settleTrail string + if len(isolate) == 2 { + // Example: ETHUSDU22_ETH quoted in USD, paid out in ETH. + settleTrail = currency.UnderscoreDelimiter + isolate[1] + } + + root := isolate[0][:len(isolate[0])-3] + contract := isolate[0][len(isolate[0])-3:] + + products = append(products, root+currency.DashDelimiter+contract+settleTrail) + } + case asset.Index: + // TODO: This can be expanded into individual assets later. + if marketInfo[x].Typ == bitMEXBasketIndexID || + marketInfo[x].Typ == bitMEXPriceIndexID || + marketInfo[x].Typ == bitMEXLendingPremiumIndexID || + marketInfo[x].Typ == bitMEXVolatilityIndexID { + products = append(products, marketInfo[x].Symbol) + } + default: + return nil, errors.New("unhandled asset type") + } + } return products, nil } // UpdateTradablePairs updates the exchanges available pairs and stores // them in the exchanges config func (b *Bitmex) UpdateTradablePairs(ctx context.Context, forceUpdate bool) error { - pairs, err := b.FetchTradablePairs(ctx, asset.Spot) - if err != nil { - return err - } + assets := b.GetAssetTypes(false) - // Zerovalue current list which will remove old asset pairs when contract - // types expire or become obsolete - var assetPairs = map[asset.Item][]string{ - asset.Index: {}, - asset.PerpetualContract: {}, - asset.Futures: {}, - } - - for x := range pairs { - if strings.Contains(pairs[x], ".") { - assetPairs[asset.Index] = append(assetPairs[asset.Index], pairs[x]) - continue - } - - if strings.Contains(pairs[x], "USD") { - assetPairs[asset.PerpetualContract] = append(assetPairs[asset.PerpetualContract], - pairs[x]) - continue - } - - assetPairs[asset.Futures] = append(assetPairs[asset.Futures], pairs[x]) - } - - for a, values := range assetPairs { - p, err := currency.NewPairsFromStrings(values) + for x := range assets { + pairsStr, err := b.FetchTradablePairs(ctx, assets[x]) if err != nil { return err } - err = b.UpdatePairs(p, a, false, false) + pairs, err := currency.NewPairsFromStrings(pairsStr) if err != nil { - log.Warnf(log.ExchangeSys, - "%s failed to update available pairs. Err: %v", - b.Name, - err) + return err + } + + err = b.UpdatePairs(pairs, assets[x], false, false) + if err != nil { + return err } } - return nil } @@ -303,7 +353,13 @@ func (b *Bitmex) UpdateTickers(ctx context.Context, a asset.Item) error { } for j := range tick { - if !pairs.Contains(tick[j].Symbol, true) { + var pair currency.Pair + pair, err = currency.NewPairFromString(tick[j].Symbol) + if err != nil { + return err + } + + if !pairs.Contains(pair, true) { continue } @@ -315,7 +371,7 @@ func (b *Bitmex) UpdateTickers(ctx context.Context, a asset.Item) error { Ask: tick[j].AskPrice, Volume: tick[j].Volume24h, Close: tick[j].PrevClosePrice, - Pair: tick[j].Symbol, + Pair: pair, LastUpdated: tick[j].Timestamp, ExchangeName: b.Name, AssetType: a}) diff --git a/exchanges/bitstamp/bitstamp_wrapper.go b/exchanges/bitstamp/bitstamp_wrapper.go index f1f6d4ed..d0ffb781 100644 --- a/exchanges/bitstamp/bitstamp_wrapper.go +++ b/exchanges/bitstamp/bitstamp_wrapper.go @@ -58,7 +58,7 @@ func (b *Bitstamp) SetDefaults() { b.API.CredentialsValidator.RequiresKey = true b.API.CredentialsValidator.RequiresSecret = true b.API.CredentialsValidator.RequiresClientID = true - requestFmt := ¤cy.PairFormat{} + requestFmt := ¤cy.EMPTYFORMAT configFmt := ¤cy.PairFormat{ Uppercase: true, Delimiter: currency.ForwardSlashDelimiter, diff --git a/exchanges/btse/btse_wrapper.go b/exchanges/btse/btse_wrapper.go index 8250ea9e..010c1f32 100644 --- a/exchanges/btse/btse_wrapper.go +++ b/exchanges/btse/btse_wrapper.go @@ -82,6 +82,7 @@ func (b *BTSE) SetDefaults() { }, ConfigFormat: ¤cy.PairFormat{ Uppercase: true, + Delimiter: currency.DashDelimiter, }, } err = b.StoreAssetPairFormat(asset.Futures, fmt2) @@ -379,7 +380,7 @@ func (b *BTSE) UpdateOrderbook(ctx context.Context, p currency.Pair, assetType a Amount: a.SellQuote[x].Size, }) } - book.Asks.SortAsks() // Sort asks for correct alignment + book.Asks.SortAsks() book.Pair = p book.Exchange = b.Name book.Asset = assetType diff --git a/exchanges/bybit/bybit_wrapper.go b/exchanges/bybit/bybit_wrapper.go index d8ff7201..b64c120c 100644 --- a/exchanges/bybit/bybit_wrapper.go +++ b/exchanges/bybit/bybit_wrapper.go @@ -303,7 +303,17 @@ func (by *Bybit) FetchTradablePairs(ctx context.Context, a asset.Item) ([]string if allPairs[x].Status != "Trading" || allPairs[x].QuoteCurrency != "USD" { continue } - symbol := allPairs[x].BaseCurrency + currency.DashDelimiter + allPairs[x].QuoteCurrency + + contractSplit := strings.Split(allPairs[x].Name, allPairs[x].BaseCurrency) + if len(contractSplit) != 2 { + log.Warnf(log.ExchangeSys, "%s base currency %s cannot split contract name %s cannot add to tradable pairs", + by.Name, + allPairs[x].BaseCurrency, + allPairs[x].Name) + continue + } + + symbol := allPairs[x].BaseCurrency + currency.DashDelimiter + contractSplit[1] pairs = append(pairs, symbol) } return pairs, nil diff --git a/exchanges/bybit/bybit_ws_futures.go b/exchanges/bybit/bybit_ws_futures.go index 30b59ec5..46ad50ee 100644 --- a/exchanges/bybit/bybit_ws_futures.go +++ b/exchanges/bybit/bybit_ws_futures.go @@ -21,10 +21,6 @@ import ( "github.com/thrasher-corp/gocryptotrader/log" ) -const ( - wsFuturesPath = "realtime" -) - // WsFuturesConnect connects to a Futures websocket feed func (by *Bybit) WsFuturesConnect() error { if !by.Websocket.IsEnabled() || !by.IsEnabled() { diff --git a/exchanges/bybit/bybit_ws_ufutures.go b/exchanges/bybit/bybit_ws_ufutures.go index 598c0efc..99851d0e 100644 --- a/exchanges/bybit/bybit_ws_ufutures.go +++ b/exchanges/bybit/bybit_ws_ufutures.go @@ -21,12 +21,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/log" ) -const ( - wsUSDTMarginedPathPublic = "realtime_public" - wsUSDTMarginedPathPrivate = "realtime_private" - - wsUSDTKline = "candle" -) +const wsUSDTKline = "candle" // WsUSDTConnect connects to a USDT websocket feed func (by *Bybit) WsUSDTConnect() error { diff --git a/exchanges/exchange.go b/exchanges/exchange.go index a1195dfb..a8065ac0 100644 --- a/exchanges/exchange.go +++ b/exchanges/exchange.go @@ -41,7 +41,11 @@ const ( 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") +var ( + errEndpointStringNotFound = errors.New("endpoint string not found") + errConfigPairFormatRequiresDelimiter = errors.New("config pair format requires delimiter") + errSymbolCannotBeMatched = errors.New("symbol cannot be matched") +) // SetClientProxyAddress sets a proxy address for REST and websocket requests func (b *Base) SetClientProxyAddress(addr string) error { @@ -140,11 +144,8 @@ func (b *Base) SupportsRESTTickerBatchUpdates() bool { // SupportsAutoPairUpdates returns whether or not the exchange supports // auto currency pair updating func (b *Base) SupportsAutoPairUpdates() bool { - if b.Features.Supports.RESTCapabilities.AutoPairUpdates || - b.Features.Supports.WebsocketCapabilities.AutoPairUpdates { - return true - } - return false + return b.Features.Supports.RESTCapabilities.AutoPairUpdates || + b.Features.Supports.WebsocketCapabilities.AutoPairUpdates } // GetLastPairsUpdateTime returns the unix timestamp of when the exchanges @@ -176,6 +177,33 @@ func (b *Base) GetPairAssetType(c currency.Pair) (asset.Item, error) { 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) { @@ -192,7 +220,7 @@ func (b *Base) GetExchangeBankAccounts(id, depositCurrency string) (*banking.Acc // SetCurrencyPairFormat checks the exchange request and config currency pair // formats and syncs it with the exchanges SetDefault settings -func (b *Base) SetCurrencyPairFormat() { +func (b *Base) SetCurrencyPairFormat() error { if b.Config.CurrencyPairs == nil { b.Config.CurrencyPairs = new(currency.PairsManager) } @@ -201,7 +229,7 @@ func (b *Base) SetCurrencyPairFormat() { if b.Config.CurrencyPairs.UseGlobalFormat { b.Config.CurrencyPairs.RequestFormat = b.CurrencyPairs.RequestFormat b.Config.CurrencyPairs.ConfigFormat = b.CurrencyPairs.ConfigFormat - return + return nil } if b.Config.CurrencyPairs.ConfigFormat != nil { @@ -216,11 +244,15 @@ func (b *Base) SetCurrencyPairFormat() { if _, err := b.Config.CurrencyPairs.Get(assetTypes[x]); err != nil { ps, err := b.CurrencyPairs.Get(assetTypes[x]) if err != nil { - continue + return err + } + err = b.Config.CurrencyPairs.Store(assetTypes[x], ps) + if err != nil { + return err } - b.Config.CurrencyPairs.Store(assetTypes[x], *ps) } } + return nil } // SetConfigPairs sets the exchanges currency pairs to the pairs set in the config @@ -235,37 +267,62 @@ func (b *Base) SetConfigPairs() error { assetTypes[x]) continue // If there are unsupported assets contained in config, skip. } - cfgPS, err := b.Config.CurrencyPairs.Get(assetTypes[x]) - if err != nil { - return err - } var enabledAsset bool if b.Config.CurrencyPairs.IsAssetEnabled(assetTypes[x]) == nil { enabledAsset = true } - err = b.CurrencyPairs.SetAssetEnabled(assetTypes[x], enabledAsset) + 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 && err != currency.ErrAssetAlreadyEnabled { + 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 { - b.CurrencyPairs.StorePairs(assetTypes[x], cfgPS.Available, false) - b.CurrencyPairs.StorePairs(assetTypes[x], cfgPS.Enabled, true) + 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 } - cfgPS.ConfigFormat = exchPS.ConfigFormat - cfgPS.RequestFormat = exchPS.RequestFormat - b.CurrencyPairs.StorePairs(assetTypes[x], cfgPS.Available, false) - b.CurrencyPairs.StorePairs(assetTypes[x], cfgPS.Enabled, true) + + 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 } @@ -291,14 +348,14 @@ func (b *Base) GetPairFormat(assetType asset.Item, requestFormat bool) (currency if b.CurrencyPairs.UseGlobalFormat { if requestFormat { if b.CurrencyPairs.RequestFormat == nil { - return currency.PairFormat{}, + return currency.EMPTYFORMAT, errors.New("global request format is nil") } return *b.CurrencyPairs.RequestFormat, nil } if b.CurrencyPairs.ConfigFormat == nil { - return currency.PairFormat{}, + return currency.EMPTYFORMAT, errors.New("global config format is nil") } return *b.CurrencyPairs.ConfigFormat, nil @@ -306,19 +363,19 @@ func (b *Base) GetPairFormat(assetType asset.Item, requestFormat bool) (currency ps, err := b.CurrencyPairs.Get(assetType) if err != nil { - return currency.PairFormat{}, err + return currency.EMPTYFORMAT, err } if requestFormat { if ps.RequestFormat == nil { - return currency.PairFormat{}, + return currency.EMPTYFORMAT, errors.New("asset type request format is nil") } return *ps.RequestFormat, nil } if ps.ConfigFormat == nil { - return currency.PairFormat{}, + return currency.EMPTYFORMAT, errors.New("asset type config format is nil") } return *ps.ConfigFormat, nil @@ -336,14 +393,11 @@ func (b *Base) GetEnabledPairs(a asset.Item) (currency.Pairs, error) { if err != nil { return nil, err } - enabledpairs, err := b.CurrencyPairs.GetPairs(a, true) + enabledPairs, err := b.CurrencyPairs.GetPairs(a, true) if err != nil { return nil, err } - return enabledpairs.Format(format.Delimiter, - format.Index, - format.Uppercase), - nil + return enabledPairs.Format(format), nil } // GetRequestFormattedPairAndAssetType is a method that returns the enabled currency pair of @@ -362,7 +416,7 @@ func (b *Base) GetRequestFormattedPairAndAssetType(p string) (currency.Pair, ass } for j := range pairs { - formattedPair := pairs[j].Format(format.Delimiter, format.Uppercase) + formattedPair := pairs[j].Format(format) if strings.EqualFold(formattedPair.String(), p) { return formattedPair, assetTypes[i], nil } @@ -382,28 +436,23 @@ func (b *Base) GetAvailablePairs(assetType asset.Item) (currency.Pairs, error) { if err != nil { return nil, err } - return pairs.Format(format.Delimiter, format.Index, format.Uppercase), nil + 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) - if err != nil { - return err - } - if pairs.Contains(p, false) { - return nil - } - return errors.New("pair not supported") + pairs, err = b.GetEnabledPairs(assetType) + } else { + pairs, err = b.GetAvailablePairs(assetType) } - - avail, err := b.GetAvailablePairs(assetType) if err != nil { return err } - if avail.Contains(p, false) { + if pairs.Contains(p, false) { return nil } return errors.New("pair not supported") @@ -443,7 +492,7 @@ func (b *Base) FormatExchangeCurrency(p currency.Pair, assetType asset.Item) (cu if err != nil { return currency.EMPTYPAIR, err } - return p.Format(pairFmt.Delimiter, pairFmt.Uppercase), nil + return p.Format(pairFmt), nil } // SetEnabled is a method that sets if the exchange is enabled @@ -503,7 +552,11 @@ func (b *Base) SetupDefaults(exch *config.Exchange) error { if err != nil { return err } - b.SetCurrencyPairFormat() + + err = b.SetCurrencyPairFormat() + if err != nil { + return err + } err = b.SetConfigPairs() if err != nil { @@ -551,95 +604,166 @@ func (b *Base) SetPairs(pairs currency.Pairs, assetType asset.Item, enabled bool return err } - var newPairs currency.Pairs for x := range pairs { - newPairs = append(newPairs, pairs[x].Format(pairFmt.Delimiter, - pairFmt.Uppercase)) + pairs[x] = pairs[x].Format(pairFmt) } - b.CurrencyPairs.StorePairs(assetType, newPairs, enabled) - b.Config.CurrencyPairs.StorePairs(assetType, newPairs, enabled) - return nil + 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(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 := b.CurrencyPairs.GetPairs(assetType, enabled) +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 } - if enabled { - updateType = "enabled" - } else { - updateType = "available" + incoming, err = incoming.ValidateAndConform(pFmt, b.BypassConfigFormatUpgrades) + if err != nil { + return err } - newPairs, removedPairs := targetPairs.FindDifferences(products) - if force || len(newPairs) > 0 || len(removedPairs) > 0 { + 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(assetType.String())) + strings.ToUpper(a.String())) } else { - if len(newPairs) > 0 { + if len(diff.New) > 0 { log.Debugf(log.ExchangeSys, "%s Updating %s pairs [%v] - Added: %s.\n", b.Name, updateType, - strings.ToUpper(assetType.String()), - newPairs) + strings.ToUpper(a.String()), + diff.New) } - if len(removedPairs) > 0 { + if len(diff.Remove) > 0 { log.Debugf(log.ExchangeSys, "%s Updating %s pairs [%v] - Removed: %s.\n", b.Name, updateType, - strings.ToUpper(assetType.String()), - removedPairs) + strings.ToUpper(a.String()), + diff.Remove) } } - - b.Config.CurrencyPairs.StorePairs(assetType, products, enabled) - b.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 := b.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", - b.Name, - strings.ToUpper(assetType.String()), - remove) - b.Config.CurrencyPairs.StorePairs(assetType, enabledPairs, true) - b.CurrencyPairs.StorePairs(assetType, enabledPairs, true) - } + 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 } } - return nil + + 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 @@ -865,6 +989,11 @@ func (b *Base) StoreAssetPairFormat(a asset.Item, f currency.PairStore) error { 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) } @@ -891,6 +1020,11 @@ func (b *Base) SetGlobalPairsManager(request, config *currency.PairFormat, asset 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 diff --git a/exchanges/exchange_test.go b/exchanges/exchange_test.go index f3d144af..9610c6c1 100644 --- a/exchanges/exchange_test.go +++ b/exchanges/exchange_test.go @@ -418,7 +418,10 @@ func TestSetCurrencyPairFormat(t *testing.T) { b := Base{ Config: &config.Exchange{}, } - b.SetCurrencyPairFormat() + err := b.SetCurrencyPairFormat() + if err != nil { + t.Fatal(err) + } if b.Config.CurrencyPairs == nil { t.Error("currencyPairs shouldn't be nil") } @@ -431,7 +434,10 @@ func TestSetCurrencyPairFormat(t *testing.T) { } b.CurrencyPairs.RequestFormat = pFmt b.CurrencyPairs.ConfigFormat = pFmt - b.SetCurrencyPairFormat() + err = b.SetCurrencyPairFormat() + if err != nil { + t.Fatal(err) + } spot, err := b.GetPairFormat(asset.Spot, true) if err != nil { t.Fatal(err) @@ -444,17 +450,22 @@ func TestSetCurrencyPairFormat(t *testing.T) { // Test individual asset type formatting logic b.CurrencyPairs.UseGlobalFormat = false // Store non-nil pair stores - b.CurrencyPairs.Store(asset.Spot, currency.PairStore{ - ConfigFormat: ¤cy.PairFormat{ - Delimiter: "~", - }, + err = b.CurrencyPairs.Store(asset.Spot, ¤cy.PairStore{ + ConfigFormat: ¤cy.PairFormat{Delimiter: "~"}, }) - b.CurrencyPairs.Store(asset.Futures, currency.PairStore{ - ConfigFormat: ¤cy.PairFormat{ - Delimiter: ":)", - }, + if err != nil { + t.Fatal(err) + } + err = b.CurrencyPairs.Store(asset.Futures, ¤cy.PairStore{ + ConfigFormat: ¤cy.PairFormat{Delimiter: ":)"}, }) - b.SetCurrencyPairFormat() + if err != nil { + t.Fatal(err) + } + err = b.SetCurrencyPairFormat() + if err != nil { + t.Fatal(err) + } spot, err = b.GetPairFormat(asset.Spot, false) if err != nil { t.Fatal(err) @@ -492,8 +503,8 @@ func TestLoadConfigPairs(t *testing.T) { }, Pairs: map[asset.Item]*currency.PairStore{ asset.Spot: { - RequestFormat: ¤cy.PairFormat{}, - ConfigFormat: ¤cy.PairFormat{}, + RequestFormat: ¤cy.EMPTYFORMAT, + ConfigFormat: ¤cy.EMPTYFORMAT, }, }, }, @@ -529,7 +540,11 @@ func TestLoadConfigPairs(t *testing.T) { } // Test UseGlobalFormat setting of pairs - b.SetCurrencyPairFormat() + err = b.SetCurrencyPairFormat() + if err != nil { + t.Fatal(err) + } + err = b.SetConfigPairs() if err != nil { t.Fatal(err) @@ -548,7 +563,7 @@ func TestLoadConfigPairs(t *testing.T) { t.Fatal(err) } - p := pairs[0].Format(pFmt.Delimiter, pFmt.Uppercase).String() + p := pairs[0].Format(pFmt).String() if p != "BTC^USD" { t.Errorf("incorrect value, expected BTC^USD") } @@ -575,17 +590,23 @@ func TestLoadConfigPairs(t *testing.T) { } // Test !UseGlobalFormat setting of pairs - exchPS, err := b.CurrencyPairs.Get(asset.Spot) + err = b.CurrencyPairs.StoreFormat(asset.Spot, ¤cy.PairFormat{Delimiter: "~"}, false) + if err != nil { + t.Fatal(err) + } + err = b.CurrencyPairs.StoreFormat(asset.Spot, ¤cy.PairFormat{Delimiter: "/"}, true) if err != nil { t.Fatal(err) } - exchPS.RequestFormat.Delimiter = "~" - exchPS.RequestFormat.Uppercase = false - exchPS.ConfigFormat.Delimiter = "/" - exchPS.ConfigFormat.Uppercase = false pairs = append(pairs, currency.Pair{Base: currency.XRP, Quote: currency.USD}) - b.Config.CurrencyPairs.StorePairs(asset.Spot, pairs, false) - b.Config.CurrencyPairs.StorePairs(asset.Spot, pairs, true) + err = b.Config.CurrencyPairs.StorePairs(asset.Spot, pairs, false) + if err != nil { + t.Fatal(err) + } + err = b.Config.CurrencyPairs.StorePairs(asset.Spot, pairs, true) + if err != nil { + t.Fatal(err) + } b.Config.CurrencyPairs.UseGlobalFormat = false b.CurrencyPairs.UseGlobalFormat = false @@ -598,7 +619,7 @@ func TestLoadConfigPairs(t *testing.T) { // 2) pair format is set for RequestFormat // 3) pair format is set for ConfigFormat // 4) Config pair store formats are the same as the exchanges - pFmt, err = b.GetPairFormat(asset.Spot, false) + configFmt, err := b.GetPairFormat(asset.Spot, false) if err != nil { t.Fatal(err) } @@ -606,7 +627,7 @@ func TestLoadConfigPairs(t *testing.T) { if err != nil { t.Fatal(err) } - p = pairs[2].Format(pFmt.Delimiter, pFmt.Uppercase).String() + p = pairs[2].Format(configFmt).String() if p != "xrp/usd" { t.Error("incorrect value, expected xrp/usd", p) } @@ -700,13 +721,13 @@ func TestGetPairFormat(t *testing.T) { // Test individual asset pair store formatting b.CurrencyPairs.UseGlobalFormat = false - b.CurrencyPairs.Store(asset.Spot, currency.PairStore{ - ConfigFormat: &pFmt, - RequestFormat: ¤cy.PairFormat{ - Delimiter: "/", - Uppercase: true, - }, + err = b.CurrencyPairs.Store(asset.Spot, ¤cy.PairStore{ + ConfigFormat: &pFmt, + RequestFormat: ¤cy.PairFormat{Delimiter: "/", Uppercase: true}, }) + if err != nil { + t.Fatal(err) + } pFmt, err = b.GetPairFormat(asset.Spot, false) if err != nil { t.Fatal(err) @@ -735,8 +756,14 @@ func TestGetEnabledPairs(t *testing.T) { t.Fatal(err) } - b.CurrencyPairs.StorePairs(asset.Spot, defaultPairs, true) - b.CurrencyPairs.StorePairs(asset.Spot, defaultPairs, false) + err = b.CurrencyPairs.StorePairs(asset.Spot, defaultPairs, true) + if err != nil { + t.Fatal(err) + } + err = b.CurrencyPairs.StorePairs(asset.Spot, defaultPairs, false) + if err != nil { + t.Fatal(err) + } format := currency.PairFormat{ Delimiter: "-", Index: "", @@ -786,8 +813,14 @@ func TestGetEnabledPairs(t *testing.T) { t.Fatal(err) } - b.CurrencyPairs.StorePairs(asset.Spot, btcdoge, true) - b.CurrencyPairs.StorePairs(asset.Spot, btcdoge, false) + err = b.CurrencyPairs.StorePairs(asset.Spot, btcdoge, true) + if err != nil { + t.Fatal(err) + } + err = b.CurrencyPairs.StorePairs(asset.Spot, btcdoge, false) + if err != nil { + t.Fatal(err) + } format.Index = currency.BTC.String() b.CurrencyPairs.ConfigFormat = &format c, err = b.GetEnabledPairs(asset.Spot) @@ -803,8 +836,14 @@ func TestGetEnabledPairs(t *testing.T) { t.Fatal(err) } - b.CurrencyPairs.StorePairs(asset.Spot, btcusdUnderscore, true) - b.CurrencyPairs.StorePairs(asset.Spot, btcusdUnderscore, false) + err = b.CurrencyPairs.StorePairs(asset.Spot, btcusdUnderscore, true) + if err != nil { + t.Fatal(err) + } + err = b.CurrencyPairs.StorePairs(asset.Spot, btcusdUnderscore, false) + if err != nil { + t.Fatal(err) + } b.CurrencyPairs.RequestFormat.Delimiter = "" b.CurrencyPairs.ConfigFormat.Delimiter = "_" c, err = b.GetEnabledPairs(asset.Spot) @@ -815,8 +854,14 @@ func TestGetEnabledPairs(t *testing.T) { t.Error("Exchange GetAvailablePairs() incorrect string") } - b.CurrencyPairs.StorePairs(asset.Spot, btcdoge, true) - b.CurrencyPairs.StorePairs(asset.Spot, btcdoge, false) + err = b.CurrencyPairs.StorePairs(asset.Spot, btcdoge, true) + if err != nil { + t.Fatal(err) + } + err = b.CurrencyPairs.StorePairs(asset.Spot, btcdoge, false) + if err != nil { + t.Fatal(err) + } b.CurrencyPairs.RequestFormat.Delimiter = "" b.CurrencyPairs.ConfigFormat.Delimiter = "" b.CurrencyPairs.ConfigFormat.Index = currency.BTC.String() @@ -833,8 +878,14 @@ func TestGetEnabledPairs(t *testing.T) { t.Fatal(err) } - b.CurrencyPairs.StorePairs(asset.Spot, btcusd, true) - b.CurrencyPairs.StorePairs(asset.Spot, btcusd, false) + err = b.CurrencyPairs.StorePairs(asset.Spot, btcusd, true) + if err != nil { + t.Fatal(err) + } + err = b.CurrencyPairs.StorePairs(asset.Spot, btcusd, false) + if err != nil { + t.Fatal(err) + } b.CurrencyPairs.ConfigFormat.Index = "" c, err = b.GetEnabledPairs(asset.Spot) if err != nil { @@ -857,7 +908,10 @@ func TestGetAvailablePairs(t *testing.T) { t.Fatal(err) } - b.CurrencyPairs.StorePairs(asset.Spot, defaultPairs, false) + err = b.CurrencyPairs.StorePairs(asset.Spot, defaultPairs, false) + if err != nil { + t.Fatal(err) + } format := currency.PairFormat{ Delimiter: "-", Index: "", @@ -905,7 +959,11 @@ func TestGetAvailablePairs(t *testing.T) { t.Fatal(err) } - b.CurrencyPairs.StorePairs(asset.Spot, dogePairs, false) + err = b.CurrencyPairs.StorePairs(asset.Spot, dogePairs, false) + if err != nil { + t.Fatal(err) + } + format.Index = currency.BTC.String() b.CurrencyPairs.ConfigFormat = &format c, err = b.GetAvailablePairs(assetType) @@ -922,7 +980,11 @@ func TestGetAvailablePairs(t *testing.T) { t.Fatal(err) } - b.CurrencyPairs.StorePairs(asset.Spot, btcusdUnderscore, false) + err = b.CurrencyPairs.StorePairs(asset.Spot, btcusdUnderscore, false) + if err != nil { + t.Fatal(err) + } + b.CurrencyPairs.RequestFormat.Delimiter = "" b.CurrencyPairs.ConfigFormat.Delimiter = "_" c, err = b.GetAvailablePairs(assetType) @@ -934,7 +996,11 @@ func TestGetAvailablePairs(t *testing.T) { t.Error("Exchange GetAvailablePairs() incorrect string") } - b.CurrencyPairs.StorePairs(asset.Spot, dogePairs, false) + err = b.CurrencyPairs.StorePairs(asset.Spot, dogePairs, false) + if err != nil { + t.Fatal(err) + } + b.CurrencyPairs.RequestFormat.Delimiter = "" b.CurrencyPairs.ConfigFormat.Delimiter = "_" b.CurrencyPairs.ConfigFormat.Index = currency.BTC.String() @@ -952,7 +1018,11 @@ func TestGetAvailablePairs(t *testing.T) { t.Fatal(err) } - b.CurrencyPairs.StorePairs(asset.Spot, btcusd, false) + err = b.CurrencyPairs.StorePairs(asset.Spot, btcusd, false) + if err != nil { + t.Fatal(err) + } + b.CurrencyPairs.ConfigFormat.Index = "" c, err = b.GetAvailablePairs(assetType) if err != nil { @@ -984,14 +1054,20 @@ func TestSupportsPair(t *testing.T) { t.Fatal(err) } - b.CurrencyPairs.StorePairs(asset.Spot, pairs, false) + err = b.CurrencyPairs.StorePairs(asset.Spot, pairs, false) + if err != nil { + t.Fatal(err) + } defaultpairs, err := currency.NewPairsFromStrings([]string{defaultTestCurrencyPair}) if err != nil { t.Fatal(err) } - b.CurrencyPairs.StorePairs(asset.Spot, defaultpairs, true) + err = b.CurrencyPairs.StorePairs(asset.Spot, defaultpairs, true) + if err != nil { + t.Fatal(err) + } format := ¤cy.PairFormat{ Delimiter: "-", @@ -1161,13 +1237,12 @@ func TestSetupDefaults(t *testing.T) { if err != nil { t.Fatal(err) } - b.CurrencyPairs.Store(asset.Spot, - currency.PairStore{ - Enabled: currency.Pairs{ - p, - }, - }, - ) + err = b.CurrencyPairs.Store(asset.Spot, ¤cy.PairStore{ + Enabled: currency.Pairs{p}, + }) + if err != nil { + t.Fatal(err) + } err = b.SetupDefaults(&cfg) if err != nil { t.Fatal(err) @@ -1289,6 +1364,8 @@ func TestUpdatePairs(t *testing.T) { AssetEnabled: convert.BoolPtr(true), }, }, + ConfigFormat: ¤cy.PairFormat{Uppercase: true, Delimiter: currency.DashDelimiter}, + UseGlobalFormat: true, }, } UAC.Config = exchCfg @@ -1363,7 +1440,7 @@ func TestUpdatePairs(t *testing.T) { } err = UAC.UpdatePairs(exchangeProducts, asset.Spot, false, false) if err != nil { - t.Errorf("Forced Exchange UpdatePairs() error: %s", err) + t.Errorf("Exchange UpdatePairs() error: %s", err) } // Test empty pair @@ -1371,18 +1448,29 @@ func TestUpdatePairs(t *testing.T) { if err != nil { t.Fatal(err) } - pairs := currency.Pairs{ - currency.EMPTYPAIR, - p, - } + pairs := currency.Pairs{currency.EMPTYPAIR, p} err = UAC.UpdatePairs(pairs, asset.Spot, true, true) - if err != nil { - t.Errorf("Forced Exchange UpdatePairs() error: %s", err) + if !errors.Is(err, currency.ErrCurrencyPairEmpty) { + t.Fatalf("received: '%v' but expected: '%v'", err, currency.ErrCurrencyPairEmpty) } + + pairs = currency.Pairs{p, p} err = UAC.UpdatePairs(pairs, asset.Spot, false, true) - if err != nil { - t.Errorf("Forced Exchange UpdatePairs() error: %s", err) + if !errors.Is(err, currency.ErrPairDuplication) { + t.Fatalf("received: '%v' but expected: '%v'", err, currency.ErrPairDuplication) } + + pairs = currency.Pairs{p} + err = UAC.UpdatePairs(pairs, asset.Spot, false, true) + if !errors.Is(err, nil) { + t.Fatalf("received: '%v' but expected: '%v'", err, nil) + } + + err = UAC.UpdatePairs(pairs, asset.Spot, true, true) + if !errors.Is(err, nil) { + t.Fatalf("received: '%v' but expected: '%v'", err, nil) + } + UAC.CurrencyPairs.UseGlobalFormat = true UAC.CurrencyPairs.ConfigFormat = ¤cy.PairFormat{ Delimiter: "-", @@ -1395,6 +1483,86 @@ func TestUpdatePairs(t *testing.T) { if !uacPairs.Contains(p, true) { t.Fatal("expected currency pair not found") } + + pairs = currency.Pairs{ + currency.NewPair(currency.XRP, currency.USD), + currency.NewPair(currency.BTC, currency.USD), + currency.NewPair(currency.LTC, currency.USD), + currency.NewPair(currency.LTC, currency.USDT), + } + err = UAC.UpdatePairs(pairs, asset.Spot, true, true) + if !errors.Is(err, nil) { + t.Fatalf("received: '%v' but expected: '%v'", err, nil) + } + + pairs = currency.Pairs{ + currency.NewPair(currency.WABI, currency.USD), + currency.NewPair(currency.EASY, currency.USD), + currency.NewPair(currency.LARIX, currency.USD), + currency.NewPair(currency.LTC, currency.USDT), + } + err = UAC.UpdatePairs(pairs, asset.Spot, false, true) + if !errors.Is(err, nil) { + t.Fatalf("received: '%v' but expected: '%v'", err, nil) + } + + uacEnabledPairs, err := UAC.GetEnabledPairs(asset.Spot) + if err != nil { + t.Fatal(err) + } + if uacEnabledPairs.Contains(currency.NewPair(currency.XRP, currency.USD), true) { + t.Fatal("expected currency pair not found") + } + if uacEnabledPairs.Contains(currency.NewPair(currency.BTC, currency.USD), true) { + t.Fatal("expected currency pair not found") + } + if uacEnabledPairs.Contains(currency.NewPair(currency.LTC, currency.USD), true) { + t.Fatal("expected currency pair not found") + } + if !uacEnabledPairs.Contains(currency.NewPair(currency.LTC, currency.USDT), true) { + t.Fatal("expected currency pair not found") + } + + // This should be matched and formatted to `link-usd` + unintentionalInput, err := currency.NewPairFromString("linkusd") + if !errors.Is(err, nil) { + t.Fatalf("received: '%v' but expected: '%v'", err, nil) + } + + pairs = currency.Pairs{ + currency.NewPair(currency.WABI, currency.USD), + currency.NewPair(currency.EASY, currency.USD), + currency.NewPair(currency.LARIX, currency.USD), + currency.NewPair(currency.LTC, currency.USDT), + unintentionalInput, + } + + err = UAC.UpdatePairs(pairs, asset.Spot, true, true) + if !errors.Is(err, nil) { + t.Fatalf("received: '%v' but expected: '%v'", err, nil) + } + + pairs = currency.Pairs{ + currency.NewPair(currency.WABI, currency.USD), + currency.NewPair(currency.EASY, currency.USD), + currency.NewPair(currency.LARIX, currency.USD), + currency.NewPair(currency.LTC, currency.USDT), + currency.NewPair(currency.LINK, currency.USD), + } + + err = UAC.UpdatePairs(pairs, asset.Spot, false, true) + if !errors.Is(err, nil) { + t.Fatalf("received: '%v' but expected: '%v'", err, nil) + } + + uacEnabledPairs, err = UAC.GetEnabledPairs(asset.Spot) + if err != nil { + t.Fatal(err) + } + + if !uacEnabledPairs.Contains(currency.NewPair(currency.LINK, currency.USD), true) { + t.Fatalf("received: '%v' but expected: '%v'", false, true) + } } func TestSupportsWebsocket(t *testing.T) { @@ -1604,7 +1772,10 @@ func TestGetFormattedPairAndAssetType(t *testing.T) { b := Base{ Config: &config.Exchange{}, } - b.SetCurrencyPairFormat() + err := b.SetCurrencyPairFormat() + if err != nil { + t.Fatal(err) + } b.Config.CurrencyPairs.UseGlobalFormat = true b.CurrencyPairs.UseGlobalFormat = true pFmt := ¤cy.PairFormat{ @@ -1662,13 +1833,20 @@ func TestStoreAssetPairFormat(t *testing.T) { err = b.StoreAssetPairFormat(asset.Spot, currency.PairStore{ RequestFormat: ¤cy.PairFormat{Uppercase: true}, ConfigFormat: ¤cy.PairFormat{Uppercase: true}}) + if !errors.Is(err, errConfigPairFormatRequiresDelimiter) { + t.Fatalf("received: '%v' but expected: '%v'", err, errConfigPairFormatRequiresDelimiter) + } + + err = b.StoreAssetPairFormat(asset.Futures, currency.PairStore{ + RequestFormat: ¤cy.PairFormat{Uppercase: true}, + ConfigFormat: ¤cy.PairFormat{Uppercase: true, Delimiter: currency.DashDelimiter}}) if err != nil { t.Error(err) } err = b.StoreAssetPairFormat(asset.Futures, currency.PairStore{ RequestFormat: ¤cy.PairFormat{Uppercase: true}, - ConfigFormat: ¤cy.PairFormat{Uppercase: true}}) + ConfigFormat: ¤cy.PairFormat{Uppercase: true, Delimiter: currency.DashDelimiter}}) if err != nil { t.Error(err) } @@ -1702,7 +1880,17 @@ func TestSetGlobalPairsManager(t *testing.T) { } err = b.SetGlobalPairsManager(¤cy.PairFormat{Uppercase: true}, - ¤cy.PairFormat{Uppercase: true}, asset.Spot, asset.Binary) + ¤cy.PairFormat{Uppercase: true}, + asset.Spot, + asset.Binary) + if !errors.Is(err, errConfigPairFormatRequiresDelimiter) { + t.Fatalf("received: '%v' but expected: '%v'", err, errConfigPairFormatRequiresDelimiter) + } + + err = b.SetGlobalPairsManager(¤cy.PairFormat{Uppercase: true}, + ¤cy.PairFormat{Uppercase: true, Delimiter: currency.DashDelimiter}, + asset.Spot, + asset.Binary) if err != nil { t.Error(err) } @@ -2164,10 +2352,11 @@ func TestAssetWebsocketFunctionality(t *testing.T) { }, ConfigFormat: ¤cy.PairFormat{ Uppercase: true, + Delimiter: currency.DashDelimiter, }, }) - if err != nil { - log.Errorln(log.ExchangeSys, err) + if !errors.Is(err, nil) { + t.Fatalf("received: '%v' but expected: '%v'", err, nil) } err = b.DisableAssetWebsocketSupport(asset.Spot) @@ -2372,3 +2561,72 @@ func TestIsPerpetualFutureCurrency(t *testing.T) { t.Errorf("received: %v, expected: %v", err, common.ErrNotYetImplemented) } } + +func TestGetPairAndAssetTypeRequestFormatted(t *testing.T) { + t.Parallel() + + expected := currency.Pair{Base: currency.BTC, Quote: currency.USDT} + enabledPairs := currency.Pairs{expected} + availablePairs := currency.Pairs{ + currency.Pair{Base: currency.BTC, Quote: currency.USDT}, + currency.Pair{Base: currency.BTC, Quote: currency.AUD}, + } + + b := Base{ + CurrencyPairs: currency.PairsManager{ + Pairs: map[asset.Item]*currency.PairStore{ + asset.Spot: { + AssetEnabled: convert.BoolPtr(true), + Enabled: enabledPairs, + Available: availablePairs, + RequestFormat: ¤cy.PairFormat{Delimiter: "-", Uppercase: true}, + ConfigFormat: ¤cy.EMPTYFORMAT, + }, + asset.PerpetualContract: { + AssetEnabled: convert.BoolPtr(true), + Enabled: enabledPairs, + Available: availablePairs, + RequestFormat: ¤cy.PairFormat{Delimiter: "_", Uppercase: true}, + ConfigFormat: ¤cy.EMPTYFORMAT, + }, + }, + }, + } + + _, _, err := b.GetPairAndAssetTypeRequestFormatted("") + if !errors.Is(err, currency.ErrCurrencyPairEmpty) { + t.Fatalf("received: '%v' but expected: '%v'", err, currency.ErrCurrencyPairEmpty) + } + + _, _, err = b.GetPairAndAssetTypeRequestFormatted("BTCAUD") + if !errors.Is(err, errSymbolCannotBeMatched) { + t.Fatalf("received: '%v' but expected: '%v'", err, errSymbolCannotBeMatched) + } + + _, _, err = b.GetPairAndAssetTypeRequestFormatted("BTCUSDT") + if !errors.Is(err, errSymbolCannotBeMatched) { + t.Fatalf("received: '%v' but expected: '%v'", err, errSymbolCannotBeMatched) + } + + p, a, err := b.GetPairAndAssetTypeRequestFormatted("BTC-USDT") + if !errors.Is(err, nil) { + t.Fatalf("received: '%v' but expected: '%v'", err, nil) + } + if a != asset.Spot { + t.Fatal("unexpected value", a) + } + if !p.Equal(expected) { + t.Fatalf("received: '%v' but expected: '%v'", p, expected) + } + + p, a, err = b.GetPairAndAssetTypeRequestFormatted("BTC_USDT") + if !errors.Is(err, nil) { + t.Fatalf("received: '%v' but expected: '%v'", err, nil) + } + if a != asset.PerpetualContract { + t.Fatal("unexpected value", a) + } + if !p.Equal(expected) { + t.Fatalf("received: '%v' but expected: '%v'", p, expected) + } +} diff --git a/exchanges/ftx/ftx_test.go b/exchanges/ftx/ftx_test.go index a068d106..a685a9ee 100644 --- a/exchanges/ftx/ftx_test.go +++ b/exchanges/ftx/ftx_test.go @@ -1348,8 +1348,12 @@ func TestGetHistoricTrades(t *testing.T) { if err != nil { t.Fatal(err) } + rPair, err := enabledPairs.GetRandomPair() + if !errors.Is(err, nil) { + t.Fatalf("received: '%v' but expected: '%v'", err, nil) + } _, err = f.GetHistoricTrades(context.Background(), - enabledPairs.GetRandomPair(), + rPair, assets[i], time.Now().Add(-time.Minute*15), time.Now()) @@ -1367,8 +1371,12 @@ func TestGetRecentTrades(t *testing.T) { if err != nil { t.Fatal(err) } + rPair, err := enabledPairs.GetRandomPair() + if !errors.Is(err, nil) { + t.Fatalf("received: '%v' but expected: '%v'", err, nil) + } _, err = f.GetRecentTrades(context.Background(), - enabledPairs.GetRandomPair(), assets[i]) + rPair, assets[i]) if err != nil { t.Error(err) } diff --git a/exchanges/ftx/ftx_wrapper.go b/exchanges/ftx/ftx_wrapper.go index b12c28e1..e4248c75 100644 --- a/exchanges/ftx/ftx_wrapper.go +++ b/exchanges/ftx/ftx_wrapper.go @@ -2067,7 +2067,7 @@ func (f *FTX) GetFundingRates(ctx context.Context, request *order.FundingRatesRe if err != nil { return nil, err } - request.Pairs = request.Pairs.Format(pairFmt.Delimiter, pairFmt.Index, pairFmt.Uppercase) + request.Pairs = request.Pairs.Format(pairFmt) response := make([]order.FundingRates, 0, len(request.Pairs)) for x := range request.Pairs { var isPerp bool diff --git a/exchanges/hitbtc/hitbtc_test.go b/exchanges/hitbtc/hitbtc_test.go index 741278f4..fffb7993 100644 --- a/exchanges/hitbtc/hitbtc_test.go +++ b/exchanges/hitbtc/hitbtc_test.go @@ -174,7 +174,10 @@ func TestUpdateTicker(t *testing.T) { if err != nil { t.Fatal(err) } - h.CurrencyPairs.StorePairs(asset.Spot, pairs, true) + err = h.CurrencyPairs.StorePairs(asset.Spot, pairs, true) + if err != nil { + t.Fatal(err) + } _, err = h.UpdateTicker(context.Background(), currency.NewPair(currency.BTC, currency.USD), asset.Spot) diff --git a/exchanges/huobi/huobi_futures.go b/exchanges/huobi/huobi_futures.go index d45fdc24..e87cced4 100644 --- a/exchanges/huobi/huobi_futures.go +++ b/exchanges/huobi/huobi_futures.go @@ -1219,7 +1219,7 @@ func (h *HUOBI) formatFuturesCode(p currency.Code) (string, error) { // formatFuturesPair handles pairs in the format as "BTC-NW" and "BTC210827" func (h *HUOBI) formatFuturesPair(p currency.Pair) (string, error) { if common.StringDataCompareInsensitive(validContractShortTypes, p.Quote.String()) { - return p.Format("_", true).String(), nil + return p.Format(currency.PairFormat{Delimiter: "_", Uppercase: true}).String(), nil } return h.FormatSymbol(p, asset.Futures) } diff --git a/exchanges/huobi/huobi_test.go b/exchanges/huobi/huobi_test.go index 191e1cac..913e0435 100644 --- a/exchanges/huobi/huobi_test.go +++ b/exchanges/huobi/huobi_test.go @@ -2752,7 +2752,10 @@ func TestFormatFuturesPair(t *testing.T) { if err != nil { t.Fatal(err) } - if r != availInstruments[0] { - t.Errorf("expected %s, got %s", availInstruments[0], r) + + // Test for upper case 'BTC' not lower case 'btc', disregarded numerals + // as they not deterministic from this endpoint. + if !strings.Contains(r, "BTC") { + t.Errorf("expected %s, got %s", "BTC220708", r) } } diff --git a/exchanges/huobi/huobi_wrapper.go b/exchanges/huobi/huobi_wrapper.go index 1679d792..e0029613 100644 --- a/exchanges/huobi/huobi_wrapper.go +++ b/exchanges/huobi/huobi_wrapper.go @@ -74,6 +74,7 @@ func (h *HUOBI) SetDefaults() { }, ConfigFormat: ¤cy.PairFormat{ Uppercase: true, + Delimiter: currency.DashDelimiter, }, } futures := currency.PairStore{ @@ -82,6 +83,7 @@ func (h *HUOBI) SetDefaults() { }, ConfigFormat: ¤cy.PairFormat{ Uppercase: true, + Delimiter: currency.DashDelimiter, }, } err := h.StoreAssetPairFormat(asset.Spot, fmt1) diff --git a/exchanges/itbit/itbit_wrapper.go b/exchanges/itbit/itbit_wrapper.go index 91f6abc9..dede5c59 100644 --- a/exchanges/itbit/itbit_wrapper.go +++ b/exchanges/itbit/itbit_wrapper.go @@ -59,7 +59,7 @@ func (i *ItBit) SetDefaults() { i.API.CredentialsValidator.RequiresSecret = true requestFmt := ¤cy.PairFormat{Uppercase: true} - configFmt := ¤cy.PairFormat{Uppercase: true} + configFmt := ¤cy.PairFormat{Uppercase: true, Delimiter: currency.DashDelimiter} err := i.SetGlobalPairsManager(requestFmt, configFmt, asset.Spot) if err != nil { log.Errorln(log.ExchangeSys, err) @@ -67,8 +67,7 @@ func (i *ItBit) SetDefaults() { i.Features = exchange.Features{ Supports: exchange.FeaturesSupported{ - REST: true, - Websocket: false, + REST: true, RESTCapabilities: protocol.Features{ TickerFetching: true, TradeFetching: true, @@ -88,9 +87,6 @@ func (i *ItBit) SetDefaults() { WithdrawPermissions: exchange.WithdrawCryptoViaWebsiteOnly | exchange.WithdrawFiatViaWebsiteOnly, }, - Enabled: exchange.FeaturesEnabled{ - AutoPairUpdates: false, - }, } i.Requester, err = request.New(i.Name, diff --git a/exchanges/localbitcoins/localbitcoins_wrapper.go b/exchanges/localbitcoins/localbitcoins_wrapper.go index a312581e..bde3ea2f 100644 --- a/exchanges/localbitcoins/localbitcoins_wrapper.go +++ b/exchanges/localbitcoins/localbitcoins_wrapper.go @@ -61,7 +61,9 @@ func (l *LocalBitcoins) SetDefaults() { l.API.CredentialsValidator.RequiresSecret = true requestFmt := ¤cy.PairFormat{Uppercase: true} - configFmt := ¤cy.PairFormat{Uppercase: true} + configFmt := ¤cy.PairFormat{ + Uppercase: true, + Delimiter: currency.DashDelimiter} err := l.SetGlobalPairsManager(requestFmt, configFmt, asset.Spot) if err != nil { log.Errorln(log.ExchangeSys, err) diff --git a/exchanges/okcoin/okcoin_test.go b/exchanges/okcoin/okcoin_test.go index 64641363..18f9a150 100644 --- a/exchanges/okcoin/okcoin_test.go +++ b/exchanges/okcoin/okcoin_test.go @@ -370,7 +370,7 @@ func TestPlaceMultipleSpotOrdersOverPairLimits(t *testing.T) { } for x := range pairs { - ord.InstrumentID = pairs[x].Format("-", false).String() + ord.InstrumentID = pairs[x].Format(currency.PairFormat{Delimiter: "-"}).String() request = append(request, ord) } @@ -674,7 +674,7 @@ func TestPlaceMultipleMarginOrdersOverPairLimits(t *testing.T) { } for x := range pairs { - ord.InstrumentID = pairs[x].Format("-", false).String() + ord.InstrumentID = pairs[x].Format(currency.PairFormat{Delimiter: "-"}).String() request = append(request, ord) } diff --git a/exchanges/okex/okex_test.go b/exchanges/okex/okex_test.go index 62f39984..7f498743 100644 --- a/exchanges/okex/okex_test.go +++ b/exchanges/okex/okex_test.go @@ -500,7 +500,7 @@ func TestPlaceMultipleSpotOrdersOverPairLimits(t *testing.T) { } for x := range pairs { - ord.InstrumentID = pairs[x].Format("-", false).String() + ord.InstrumentID = pairs[x].Format(currency.PairFormat{Delimiter: "-"}).String() request = append(request, ord) } @@ -884,7 +884,7 @@ func TestPlaceMultipleMarginOrdersOverPairLimits(t *testing.T) { } for x := range pairs { - ord.InstrumentID = pairs[x].Format("-", false).String() + ord.InstrumentID = pairs[x].Format(currency.PairFormat{Delimiter: "-"}).String() request = append(request, ord) } diff --git a/exchanges/okex/okex_wrapper.go b/exchanges/okex/okex_wrapper.go index c2638da2..34a28c97 100644 --- a/exchanges/okex/okex_wrapper.go +++ b/exchanges/okex/okex_wrapper.go @@ -98,6 +98,7 @@ func (o *OKEX) SetDefaults() { }, ConfigFormat: ¤cy.PairFormat{ Uppercase: true, + Delimiter: currency.DashDelimiter, }, } diff --git a/exchanges/stream/buffer/buffer_test.go b/exchanges/stream/buffer/buffer_test.go index afa366b4..566f1cad 100644 --- a/exchanges/stream/buffer/buffer_test.go +++ b/exchanges/stream/buffer/buffer_test.go @@ -1054,7 +1054,7 @@ func TestUpdateByIDAndAction(t *testing.T) { t.Fatal("did not adjust ask item placement and details") } - book.LoadSnapshot(append(bids[:0:0], bids...), append(bids[:0:0], bids...), 0, time.Time{}, true) //nolint:gocritic + book.LoadSnapshot(append(bids[:0:0], bids...), append(asks[:0:0], asks...), 0, time.Time{}, true) //nolint:gocritic // Delete - not found err = holder.updateByIDAndAction(&orderbook.Update{ Action: orderbook.Delete, @@ -1070,7 +1070,7 @@ func TestUpdateByIDAndAction(t *testing.T) { t.Fatalf("received: '%v' but expected: '%v'", err, errDeleteFailure) } - book.LoadSnapshot(append(bids[:0:0], bids...), append(bids[:0:0], bids...), 0, time.Time{}, true) //nolint:gocritic + book.LoadSnapshot(append(bids[:0:0], bids...), append(asks[:0:0], asks...), 0, time.Time{}, true) //nolint:gocritic // Delete - found err = holder.updateByIDAndAction(&orderbook.Update{ Action: orderbook.Delete, diff --git a/exchanges/zb/zb_wrapper.go b/exchanges/zb/zb_wrapper.go index 7eeecab5..63f6682f 100644 --- a/exchanges/zb/zb_wrapper.go +++ b/exchanges/zb/zb_wrapper.go @@ -263,7 +263,7 @@ func (z *ZB) UpdateTickers(ctx context.Context, a asset.Item) error { for x := range enabledPairs { // We can't use either pair format here, so format it to lower- // case and without any delimiter - curr := enabledPairs[x].Format("", false).String() + curr := enabledPairs[x].Format(currency.EMPTYFORMAT).String() if _, ok := result[curr]; !ok { continue }