diff --git a/backtester/eventhandlers/exchange/slippage/slippage_test.go b/backtester/eventhandlers/exchange/slippage/slippage_test.go index edc71299..f1e4f254 100644 --- a/backtester/eventhandlers/exchange/slippage/slippage_test.go +++ b/backtester/eventhandlers/exchange/slippage/slippage_test.go @@ -5,6 +5,8 @@ import ( "testing" "github.com/shopspring/decimal" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/bitstamp" @@ -14,31 +16,23 @@ import ( func TestRandomSlippage(t *testing.T) { t.Parallel() resp := EstimateSlippagePercentage(decimal.NewFromInt(80), decimal.NewFromInt(100)) - if resp.LessThan(decimal.NewFromFloat(0.8)) || resp.GreaterThan(decimal.NewFromInt(1)) { - t.Error("expected result > 0.8 and < 100") - } + assert.True(t, resp.GreaterThan(decimal.NewFromFloat(0.8)), "result should be more than 0.8") + assert.True(t, resp.LessThan(decimal.NewFromInt(1)), "result should be less than 1") } func TestCalculateSlippageByOrderbook(t *testing.T) { t.Parallel() b := bitstamp.Bitstamp{} b.SetDefaults() - err := b.CurrencyPairs.SetAssetEnabled(asset.Spot, true) - if err != nil { - t.Fatal(err) - } + cp := currency.NewPair(currency.BTC, currency.USD) ob, err := b.FetchOrderbook(context.Background(), cp, asset.Spot) - if err != nil { - t.Error(err) - } + require.NoError(t, err, "FetchOrderbook must not error") + amountOfFunds := decimal.NewFromInt(1000) feeRate := decimal.NewFromFloat(0.03) price, amount, err := CalculateSlippageByOrderbook(ob, gctorder.Buy, amountOfFunds, feeRate) - if err != nil { - t.Fatal(err) - } - if price.Mul(amount).Add(price.Mul(amount).Mul(feeRate)).GreaterThan(amountOfFunds) { - t.Error("order size must be less than funds") - } + require.NoError(t, err, "CalculateSlippageByOrderbook must not error") + orderSize := price.Mul(amount).Add(price.Mul(amount).Mul(feeRate)) + assert.True(t, orderSize.LessThan(amountOfFunds), "order size must be less than funds") } diff --git a/currency/manager.go b/currency/manager.go index 2bda3017..cacf13cb 100644 --- a/currency/manager.go +++ b/currency/manager.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "fmt" + "slices" "strings" "github.com/thrasher-corp/gocryptotrader/common" @@ -11,32 +12,22 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/asset" ) +// Public errors var ( - // ErrAssetAlreadyEnabled defines an error for the pairs management system - // that declares the asset is already enabled. - ErrAssetAlreadyEnabled = errors.New("asset already enabled") - // ErrPairAlreadyEnabled returns when enabling a pair that is already enabled - ErrPairAlreadyEnabled = errors.New("pair already enabled") - // ErrPairNotEnabled returns when looking for a pair that is not enabled - ErrPairNotEnabled = errors.New("pair not enabled") - // ErrPairNotFound is returned when a currency pair is not found - ErrPairNotFound = errors.New("pair not found") - // 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. + ErrAssetAlreadyEnabled = errors.New("asset already enabled") + ErrAssetIsNil = errors.New("asset is nil") + ErrAssetNotFound = errors.New("asset type not found in pair store") + ErrPairAlreadyEnabled = errors.New("pair already enabled") + ErrPairFormatIsNil = errors.New("pair format is nil") + ErrPairManagerNotInitialised = errors.New("pair manager not initialised") ErrPairNotContainedInAvailablePairs = errors.New("pair not contained in available pairs") - // ErrPairManagerNotInitialised is returned when a pairs manager is requested, but has not been setup - ErrPairManagerNotInitialised = errors.New("pair manager not initialised") - // ErrAssetNotFound is returned when an asset does not exist in the pairstore - ErrAssetNotFound = errors.New("asset type not found in pair store") - // ErrSymbolStringEmpty is an error when a symbol string is empty - ErrSymbolStringEmpty = errors.New("symbol string is empty") + ErrPairNotEnabled = errors.New("pair not enabled") + ErrPairNotFound = errors.New("pair not found") + ErrSymbolStringEmpty = errors.New("symbol string is empty") +) +var ( errPairStoreIsNil = errors.New("pair store is nil") - errPairFormatIsNil = errors.New("pair format is nil") errPairMatcherIsNil = errors.New("pair matcher is nil") errPairConfigFormatNil = errors.New("pair config format is nil") ) @@ -65,10 +56,9 @@ func (p *PairsManager) Get(a asset.Item) (*PairStore, error) { defer p.mutex.RUnlock() c, ok := p.Pairs[a] if !ok { - return nil, - fmt.Errorf("cannot get pair store, %v %w", a, asset.ErrNotSupported) + return nil, fmt.Errorf("cannot get pair store, %v %w", a, asset.ErrNotSupported) } - return c.copy() + return c.clone(), nil } // Match returns a currency pair based on the supplied symbol and asset type @@ -94,24 +84,16 @@ 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 + if ps == nil { + return errPairStoreIsNil } p.mutex.Lock() + defer p.mutex.Unlock() if p.Pairs == nil { - p.Pairs = make(map[asset.Item]*PairStore) + p.Pairs = FullStore{} } - p.Pairs[a] = cpy - if p.matcher == nil { - p.matcher = make(map[key]*Pair) - } - for x := range cpy.Available { - p.matcher[key{ - Symbol: cpy.Available[x].Base.Lower().String() + cpy.Available[x].Quote.Lower().String(), - Asset: a}] = &cpy.Available[x] - } - p.mutex.Unlock() + p.Pairs[a] = ps.clone() + p.reindex() return nil } @@ -145,9 +127,7 @@ func (p *PairsManager) GetPairs(a asset.Item, enabled bool) (Pairs, error) { } if !enabled { - availPairs := make(Pairs, len(pairStore.Available)) - copy(availPairs, pairStore.Available) - return availPairs, nil + return slices.Clone(pairStore.Available), nil } lenCheck := len(pairStore.Enabled) @@ -157,8 +137,7 @@ func (p *PairsManager) GetPairs(a asset.Item, enabled bool) (Pairs, error) { // NOTE: enabledPairs is declared before the next check for comparison // reasons within exchange update pairs functionality. - enabledPairs := make(Pairs, lenCheck) - copy(enabledPairs, pairStore.Enabled) + enabledPairs := slices.Clone(pairStore.Enabled) err := pairStore.Available.ContainsAll(pairStore.Enabled, true) if err != nil { @@ -173,11 +152,9 @@ func (p *PairsManager) StoreFormat(a asset.Item, pFmt *PairFormat, config bool) return fmt.Errorf("%s %w", a, asset.ErrNotSupported) } if pFmt == nil { - return errPairFormatIsNil + return ErrPairFormatIsNil } - cpy := *pFmt - p.mutex.Lock() defer p.mutex.Unlock() @@ -192,13 +169,42 @@ func (p *PairsManager) StoreFormat(a asset.Item, pFmt *PairFormat, config bool) } if config { - pairStore.ConfigFormat = &cpy + pairStore.ConfigFormat = pFmt.clone() } else { - pairStore.RequestFormat = &cpy + pairStore.RequestFormat = pFmt.clone() } return nil } +// GetFormat returns the pair format in a concurrent safe manner +func (p *PairsManager) GetFormat(a asset.Item, request bool) (PairFormat, error) { + p.mutex.RLock() + defer p.mutex.RUnlock() + + var pFmt *PairFormat + if p.UseGlobalFormat { + if request { + pFmt = p.RequestFormat + } else { + pFmt = p.ConfigFormat + } + } else { + ps, err := p.Get(a) + if err != nil { + return EMPTYFORMAT, err + } + if request { + pFmt = ps.RequestFormat + } else { + pFmt = ps.ConfigFormat + } + } + if pFmt == nil { + return EMPTYFORMAT, ErrPairFormatIsNil + } + return *pFmt, 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 { @@ -206,11 +212,6 @@ func (p *PairsManager) StorePairs(a asset.Item, pairs Pairs, enabled bool) error 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.mutex.Lock() defer p.mutex.Unlock() @@ -225,18 +226,10 @@ func (p *PairsManager) StorePairs(a asset.Item, pairs Pairs, enabled bool) error } if enabled { - pairStore.Enabled = cpy + pairStore.Enabled = slices.Clone(pairs) } else { - pairStore.Available = cpy - - if p.matcher == nil { - p.matcher = make(map[key]*Pair) - } - for x := range pairStore.Available { - p.matcher[key{ - Symbol: pairStore.Available[x].Base.Lower().String() + pairStore.Available[x].Quote.Lower().String(), - Asset: a}] = &pairStore.Available[x] - } + pairStore.Available = slices.Clone(pairs) + p.reindex() } return nil @@ -405,6 +398,15 @@ func (p *PairsManager) IsAssetEnabled(a asset.Item) error { return nil } +// IsAssetSupported returns if the asset is supported by an exchange +// Does not imply that the Asset is enabled +func (p *PairsManager) IsAssetSupported(a asset.Item) bool { + p.mutex.RLock() + defer p.mutex.RUnlock() + _, ok := p.Pairs[a] + return ok +} + // 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() { @@ -435,34 +437,31 @@ func (p *PairsManager) SetAssetEnabled(a asset.Item, enabled bool) error { } // Load sets the pair manager from a seed without copying mutexes -func (p *PairsManager) Load(seed *PairsManager) error { - if seed == nil { - return fmt.Errorf("%w PairsManager", common.ErrNilPointer) - } +func (p *PairsManager) Load(seed *PairsManager) { p.mutex.Lock() defer p.mutex.Unlock() seed.mutex.RLock() defer seed.mutex.RUnlock() - var pN PairsManager - j, err := json.Marshal(seed) - if err != nil { - return err - } - err = json.Unmarshal(j, &pN) - if err != nil { - return err - } - p.BypassConfigFormatUpgrades = pN.BypassConfigFormatUpgrades - if pN.UseGlobalFormat { - p.UseGlobalFormat = pN.UseGlobalFormat - p.RequestFormat = pN.RequestFormat - p.ConfigFormat = pN.ConfigFormat - } - p.LastUpdated = pN.LastUpdated - p.Pairs = pN.Pairs + p.BypassConfigFormatUpgrades = seed.BypassConfigFormatUpgrades + p.UseGlobalFormat = seed.UseGlobalFormat + p.LastUpdated = seed.LastUpdated + p.Pairs = seed.Pairs.clone() + p.RequestFormat = seed.RequestFormat.clone() + p.ConfigFormat = seed.ConfigFormat.clone() + p.reindex() +} - return nil +// reindex re-indexes the matcher for Available pairs and all assets +// This method does not lock for concurrency +func (p *PairsManager) reindex() { + p.matcher = make(map[key]*Pair) + for a, fs := range p.Pairs { + for i, pair := range fs.Available { + k := key{Symbol: pair.Base.Lower().String() + pair.Quote.Lower().String(), Asset: a} + p.matcher[k] = &fs.Available[i] + } + } } func (p *PairsManager) getPairStoreRequiresLock(a asset.Item) (*PairStore, error) { @@ -539,39 +538,30 @@ 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) { +// clone returns a deep clone of the PairStore +func (ps *PairStore) clone() *PairStore { if ps == nil { - return nil, errPairStoreIsNil + return nil } + 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 + Enabled: slices.Clone(ps.Enabled), + Available: slices.Clone(ps.Available), + RequestFormat: ps.RequestFormat.clone(), + ConfigFormat: ps.ConfigFormat.clone(), + } +} + +func (fs FullStore) clone() FullStore { + c := FullStore{} + for a, pairStore := range fs { + c[a] = pairStore.clone() + } + return c } diff --git a/currency/manager_test.go b/currency/manager_test.go index 8f17595a..4f50b929 100644 --- a/currency/manager_test.go +++ b/currency/manager_test.go @@ -7,7 +7,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/common/convert" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" ) @@ -272,8 +271,8 @@ func TestStoreFormat(t *testing.T) { } err = p.StoreFormat(asset.Spot, nil, true) - if !errors.Is(err, errPairFormatIsNil) { - t.Fatalf("received: %v but expected: %v", err, errPairFormatIsNil) + if !errors.Is(err, ErrPairFormatIsNil) { + t.Fatalf("received: %v but expected: %v", err, ErrPairFormatIsNil) } err = p.StoreFormat(asset.Spot, &PairFormat{Delimiter: "~"}, true) @@ -799,15 +798,16 @@ func TestLoad(t *testing.T) { }, } - assert.ErrorIs(t, base.Load(nil), common.ErrNilPointer, "Load nil should error") - if assert.NoError(t, base.Load(&seed), "Loading from seed should not error") { - assert.True(t, *base.Pairs[asset.Futures].AssetEnabled, "Futures AssetEnabled should be true") - assert.True(t, base.Pairs[asset.Futures].Available.Contains(p, true), "Futures Available Pairs should contain BTCUSDT") - assert.False(t, *base.Pairs[asset.Options].AssetEnabled, "Options AssetEnabled should be false") - assert.Equal(t, tt, base.LastUpdated, "Last Updated should be correct") - assert.Equal(t, fmt1.Uppercase, base.ConfigFormat.Uppercase, "ConfigFormat Uppercase should be correct") - assert.Equal(t, fmt2.Delimiter, base.RequestFormat.Delimiter, "RequestFormat Delimiter should be correct") - } + base.Load(&seed) + assert.True(t, *base.Pairs[asset.Futures].AssetEnabled, "Futures AssetEnabled should be true") + assert.True(t, base.Pairs[asset.Futures].Available.Contains(p, true), "Futures Available Pairs should contain BTCUSDT") + assert.False(t, *base.Pairs[asset.Options].AssetEnabled, "Options AssetEnabled should be false") + assert.Equal(t, tt, base.LastUpdated, "Last Updated should be correct") + assert.Equal(t, fmt1.Uppercase, base.ConfigFormat.Uppercase, "ConfigFormat Uppercase should be correct") + assert.Equal(t, fmt2.Delimiter, base.RequestFormat.Delimiter, "RequestFormat Delimiter should be correct") + found, err := base.Match("BTCUSDT", asset.Futures) + require.NoError(t, err, "Match must not error") + assert.Equal(t, p, found, "Should find the right pair") } func checkPairDelimiter(tb testing.TB, p *PairsManager, err error, d, msg string) { @@ -857,3 +857,74 @@ func TestPairManagerSetDelimitersFromConfig(t *testing.T) { assert.ErrorContains(t, err, "spot.enabled.BTC-USDT: delimiter: [_] not found in currencypair string", "SetDelimitersFromConfig should error correctly") } } + +// TestGetFormat exercises PairsManager GetFormat +func TestGetFormat(t *testing.T) { + t.Parallel() + + m := PairsManager{ + UseGlobalFormat: true, + ConfigFormat: &PairFormat{ + Uppercase: true, + Delimiter: "🦄", + }, + RequestFormat: &PairFormat{ + Delimiter: "~", + }, + } + + pFmt, err := m.GetFormat(asset.Spot, true) + require.NoError(t, err) + assert.Equal(t, "~", pFmt.Delimiter, "Global Format Delimiter should be correct") + assert.False(t, pFmt.Uppercase, "Global Format Uppercase should be correct") + + pFmt, err = m.GetFormat(asset.Spot, false) + require.NoError(t, err) + assert.Equal(t, "🦄", pFmt.Delimiter, "Global Format Delimiter should be special") + assert.True(t, pFmt.Uppercase, "Global Format Uppercase should be correct") + + m.ConfigFormat = nil + m.RequestFormat = nil + _, err = m.GetFormat(asset.Spot, true) + assert.ErrorIs(t, err, ErrPairFormatIsNil, "Global GetFormat Should error correctly on nil request format") + _, err = m.GetFormat(asset.Spot, false) + assert.ErrorIs(t, err, ErrPairFormatIsNil, "Global GetFormat Should error correctly on nil config format") + + m.UseGlobalFormat = false + err = m.Store(asset.Spot, &PairStore{ + ConfigFormat: &pFmt, + RequestFormat: &PairFormat{Delimiter: "/", Uppercase: true}, + }) + require.NoError(t, err, "Store must not error") + + pFmt, err = m.GetFormat(asset.Spot, false) + require.NoError(t, err) + assert.Equal(t, "🦄", pFmt.Delimiter, "Per Asset Format Delimiter should be correct") + assert.True(t, pFmt.Uppercase, "Per Asset Format Uppercase should be correct") + + pFmt, err = m.GetFormat(asset.Spot, true) + require.NoError(t, err) + assert.Equal(t, "/", pFmt.Delimiter, "Per Asset Format Delimiter should be correct") + assert.True(t, pFmt.Uppercase, "Per Asset Format Uppercase should be correct") + + err = m.Store(asset.Spot, &PairStore{}) + require.NoError(t, err, "Store must not error") + _, err = m.GetFormat(asset.Spot, true) + assert.ErrorIs(t, err, ErrPairFormatIsNil, "Per Asset GetFormat Should error correctly on nil request format") + _, err = m.GetFormat(asset.Spot, false) + assert.ErrorIs(t, err, ErrPairFormatIsNil, "Per Asset GetFormat Should error correctly on nil config format") +} + +// TestIsAssetSupported exercises IsAssetSupported +func TestIsAssetSupported(t *testing.T) { + t.Parallel() + p := PairsManager{ + Pairs: FullStore{ + asset.Spot: { + AssetEnabled: convert.BoolPtr(false), + }, + }, + } + assert.True(t, p.IsAssetSupported(asset.Spot), "Spot should be supported") + assert.False(t, p.IsAssetSupported(asset.Index), "Index should not be supported") +} diff --git a/currency/pair.go b/currency/pair.go index 1f8195ab..1f0a4f8c 100644 --- a/currency/pair.go +++ b/currency/pair.go @@ -118,6 +118,15 @@ func (f PairFormat) Format(pair Pair) string { return pair.Format(f).String() } +// clone returns a clone of the PairFormat +func (f *PairFormat) clone() *PairFormat { + if f == nil { + return nil + } + c := *f + return &c +} + // MatchPairsWithNoDelimiter will move along a predictable index on the provided currencyPair // it will then split on that index and verify whether that currencypair exists in the // supplied pairs diff --git a/exchanges/bitfinex/bitfinex_test.go b/exchanges/bitfinex/bitfinex_test.go index ea6edffe..d6767a49 100644 --- a/exchanges/bitfinex/bitfinex_test.go +++ b/exchanges/bitfinex/bitfinex_test.go @@ -547,6 +547,8 @@ func TestUpdateTicker(t *testing.T) { func TestUpdateTickers(t *testing.T) { t.Parallel() + b := new(Bitfinex) //nolint:govet // Intentional shadow to avoid future copy/paste mistakes + require.NoError(t, testexch.Setup(b), "Test instance Setup must not error") testexch.UpdatePairsOnce(t, b) assets := b.GetAssetTypes(false) diff --git a/exchanges/bitflyer/bitflyer_test.go b/exchanges/bitflyer/bitflyer_test.go index d343c189..fe939bd0 100644 --- a/exchanges/bitflyer/bitflyer_test.go +++ b/exchanges/bitflyer/bitflyer_test.go @@ -455,14 +455,14 @@ func TestUpdateTradablePairs(t *testing.T) { func TestGetCurrencyTradeURL(t *testing.T) { t.Parallel() testexch.UpdatePairsOnce(t, b) - err := b.CurrencyPairs.SetAssetEnabled(asset.Futures, true) - require.NoError(t, err) + err := b.CurrencyPairs.SetAssetEnabled(asset.Futures, false) + require.NoError(t, err, "SetAssetEnabled must not error") for _, a := range b.GetAssetTypes(false) { pairs, err := b.CurrencyPairs.GetPairs(a, false) require.NoError(t, err, "cannot get pairs for %s", a) require.NotEmpty(t, pairs, "no pairs for %s", a) resp, err := b.GetCurrencyTradeURL(context.Background(), a, pairs[0]) - require.NoError(t, err) - assert.NotEmpty(t, resp) + require.NoError(t, err, "GetCurrencyTradeURL must not error") + assert.NotEmpty(t, resp, "GetCurrencyTradeURL should return an url") } } diff --git a/exchanges/bitstamp/bitstamp_test.go b/exchanges/bitstamp/bitstamp_test.go index adffda23..04281129 100644 --- a/exchanges/bitstamp/bitstamp_test.go +++ b/exchanges/bitstamp/bitstamp_test.go @@ -776,84 +776,79 @@ func TestWsOrderbook2(t *testing.T) { func TestWsOrderUpdate(t *testing.T) { t.Parallel() - n := new(Bitstamp) - sharedtestvalues.TestFixtureToDataHandler(t, b, n, "testdata/wsMyOrders.json", n.wsHandleData) - seen := 0 - for reading := true; reading; { - select { - default: - reading = false - case resp := <-n.GetBase().Websocket.DataHandler: - seen++ - switch v := resp.(type) { - case *order.Detail: - switch seen { - case 1: - assert.Equal(t, "1658864794234880", v.OrderID, "OrderID") - assert.Equal(t, time.UnixMicro(1693831262313000), v.Date, "Date") - assert.Equal(t, "test_market_buy", v.ClientOrderID, "ClientOrderID") - assert.Equal(t, order.New, v.Status, "Status") - assert.Equal(t, order.Buy, v.Side, "Side") - assert.Equal(t, asset.Spot, v.AssetType, "AssetType") - assert.Equal(t, currency.NewPairWithDelimiter("BTC", "USD", "/"), v.Pair, "Pair") - assert.Equal(t, 0.0, v.ExecutedAmount, "ExecutedAmount") - assert.Equal(t, 999999999.0, v.Price, "Price") // Market Buy Price - // Note: Amount is 0 for market order create messages, oddly - case 2: - assert.Equal(t, "1658864794234880", v.OrderID, "OrderID") - assert.Equal(t, order.PartiallyFilled, v.Status, "Status") - assert.Equal(t, 0.00038667, v.Amount, "Amount") - assert.Equal(t, 0.00000001, v.RemainingAmount, "RemainingAmount") // During live tests we consistently got back this Sat remaining - assert.Equal(t, 0.00038666, v.ExecutedAmount, "ExecutedAmount") - assert.Equal(t, 25862.0, v.Price, "Price") - case 3: - assert.Equal(t, "1658864794234880", v.OrderID, "OrderID") - assert.Equal(t, order.Cancelled, v.Status, "Status") // Even though they probably consider it filled, Deleted + PartialFill = Cancelled - assert.Equal(t, 0.00038667, v.Amount, "Amount") - assert.Equal(t, 0.00000001, v.RemainingAmount, "RemainingAmount") - assert.Equal(t, 0.00038666, v.ExecutedAmount, "ExecutedAmount") - assert.Equal(t, 25862.0, v.Price, "Price") - case 4: - assert.Equal(t, "1658870500933632", v.OrderID, "OrderID") - assert.Equal(t, order.New, v.Status, "Status") - assert.Equal(t, order.Sell, v.Side, "Side") - assert.Equal(t, 0.0, v.Price, "Price") // Market Sell Price - case 5: - assert.Equal(t, "1658870500933632", v.OrderID, "OrderID") - assert.Equal(t, order.PartiallyFilled, v.Status, "Status") - assert.Equal(t, 0.00038679, v.Amount, "Amount") - assert.Equal(t, 0.00000001, v.RemainingAmount, "RemainingAmount") - assert.Equal(t, 0.00038678, v.ExecutedAmount, "ExecutedAmount") - assert.Equal(t, 25854.0, v.Price, "Price") - case 6: - assert.Equal(t, "1658870500933632", v.OrderID, "OrderID") - assert.Equal(t, order.Cancelled, v.Status, "Status") - assert.Equal(t, 0.00038679, v.Amount, "Amount") - assert.Equal(t, 0.00000001, v.RemainingAmount, "RemainingAmount") - assert.Equal(t, 0.00038678, v.ExecutedAmount, "ExecutedAmount") - assert.Equal(t, 25854.0, v.Price, "Price") - case 7: - assert.Equal(t, "1658869033291777", v.OrderID, "OrderID") - assert.Equal(t, order.New, v.Status, "Status") - assert.Equal(t, order.Sell, v.Side, "Side") - assert.Equal(t, 25845.0, v.Price, "Price") - assert.Equal(t, 0.00038692, v.Amount, "Amount") - case 8: - assert.Equal(t, "1658869033291777", v.OrderID, "OrderID") - assert.Equal(t, order.Filled, v.Status, "Status") - assert.Equal(t, 25845.0, v.Price, "Price") - assert.Equal(t, 0.00038692, v.Amount, "Amount") - assert.Equal(t, 0.0, v.RemainingAmount, "RemainingAmount") - assert.Equal(t, 0.00038692, v.ExecutedAmount, "ExecutedAmount") - } - case error: - t.Error(v) - default: - t.Errorf("Got unexpected data: %T %v", v, v) + b := new(Bitstamp) //nolint:govet // Intentional shadow to avoid future copy/paste mistakes + require.NoError(t, testexch.Setup(b), "Test instance Setup must not error") + testexch.FixtureToDataHandler(t, "testdata/wsMyOrders.json", b.wsHandleData) + close(b.Websocket.DataHandler) + assert.Len(t, b.Websocket.DataHandler, 8, "Should see 8 orders") + for resp := range b.Websocket.DataHandler { + switch v := resp.(type) { + case *order.Detail: + switch len(b.Websocket.DataHandler) { + case 7: + assert.Equal(t, "1658864794234880", v.OrderID, "OrderID") + assert.Equal(t, time.UnixMicro(1693831262313000), v.Date, "Date") + assert.Equal(t, "test_market_buy", v.ClientOrderID, "ClientOrderID") + assert.Equal(t, order.New, v.Status, "Status") + assert.Equal(t, order.Buy, v.Side, "Side") + assert.Equal(t, asset.Spot, v.AssetType, "AssetType") + assert.Equal(t, currency.NewPairWithDelimiter("BTC", "USD", "/"), v.Pair, "Pair") + assert.Equal(t, 0.0, v.ExecutedAmount, "ExecutedAmount") + assert.Equal(t, 999999999.0, v.Price, "Price") // Market Buy Price + // Note: Amount is 0 for market order create messages, oddly + case 6: + assert.Equal(t, "1658864794234880", v.OrderID, "OrderID") + assert.Equal(t, order.PartiallyFilled, v.Status, "Status") + assert.Equal(t, 0.00038667, v.Amount, "Amount") + assert.Equal(t, 0.00000001, v.RemainingAmount, "RemainingAmount") // During live tests we consistently got back this Sat remaining + assert.Equal(t, 0.00038666, v.ExecutedAmount, "ExecutedAmount") + assert.Equal(t, 25862.0, v.Price, "Price") + case 5: + assert.Equal(t, "1658864794234880", v.OrderID, "OrderID") + assert.Equal(t, order.Cancelled, v.Status, "Status") // Even though they probably consider it filled, Deleted + PartialFill = Cancelled + assert.Equal(t, 0.00038667, v.Amount, "Amount") + assert.Equal(t, 0.00000001, v.RemainingAmount, "RemainingAmount") + assert.Equal(t, 0.00038666, v.ExecutedAmount, "ExecutedAmount") + assert.Equal(t, 25862.0, v.Price, "Price") + case 4: + assert.Equal(t, "1658870500933632", v.OrderID, "OrderID") + assert.Equal(t, order.New, v.Status, "Status") + assert.Equal(t, order.Sell, v.Side, "Side") + assert.Equal(t, 0.0, v.Price, "Price") // Market Sell Price + case 3: + assert.Equal(t, "1658870500933632", v.OrderID, "OrderID") + assert.Equal(t, order.PartiallyFilled, v.Status, "Status") + assert.Equal(t, 0.00038679, v.Amount, "Amount") + assert.Equal(t, 0.00000001, v.RemainingAmount, "RemainingAmount") + assert.Equal(t, 0.00038678, v.ExecutedAmount, "ExecutedAmount") + assert.Equal(t, 25854.0, v.Price, "Price") + case 2: + assert.Equal(t, "1658870500933632", v.OrderID, "OrderID") + assert.Equal(t, order.Cancelled, v.Status, "Status") + assert.Equal(t, 0.00038679, v.Amount, "Amount") + assert.Equal(t, 0.00000001, v.RemainingAmount, "RemainingAmount") + assert.Equal(t, 0.00038678, v.ExecutedAmount, "ExecutedAmount") + assert.Equal(t, 25854.0, v.Price, "Price") + case 1: + assert.Equal(t, "1658869033291777", v.OrderID, "OrderID") + assert.Equal(t, order.New, v.Status, "Status") + assert.Equal(t, order.Sell, v.Side, "Side") + assert.Equal(t, 25845.0, v.Price, "Price") + assert.Equal(t, 0.00038692, v.Amount, "Amount") + case 0: + assert.Equal(t, "1658869033291777", v.OrderID, "OrderID") + assert.Equal(t, order.Filled, v.Status, "Status") + assert.Equal(t, 25845.0, v.Price, "Price") + assert.Equal(t, 0.00038692, v.Amount, "Amount") + assert.Equal(t, 0.0, v.RemainingAmount, "RemainingAmount") + assert.Equal(t, 0.00038692, v.ExecutedAmount, "ExecutedAmount") } + case error: + t.Error(v) + default: + t.Errorf("Got unexpected data: %T %v", v, v) } } - assert.Equal(t, 8, seen, "Number of messages") } func TestWsRequestReconnect(t *testing.T) { diff --git a/exchanges/exchange.go b/exchanges/exchange.go index cf59ae0a..5644d6af 100644 --- a/exchanges/exchange.go +++ b/exchanges/exchange.go @@ -55,10 +55,6 @@ var ( errEndpointStringNotFound = errors.New("endpoint string not found") errConfigPairFormatRequiresDelimiter = errors.New("config pair format requires delimiter") errSymbolCannotBeMatched = errors.New("symbol cannot be matched") - errGlobalRequestFormatIsNil = errors.New("global request format is nil") - errGlobalConfigFormatIsNil = errors.New("global config format is nil") - errAssetRequestFormatIsNil = errors.New("asset type request format is nil") - errAssetConfigFormatIsNil = errors.New("asset type config format is nil") errSetDefaultsNotCalled = errors.New("set defaults not called") errExchangeIsNil = errors.New("exchange is nil") errBatchSizeZero = errors.New("batch size cannot be 0") @@ -397,39 +393,9 @@ func (b *Base) GetSupportedFeatures() FeaturesSupported { return b.Features.Supports } -// GetPairFormat returns the pair format based on the exchange and -// asset type -func (b *Base) GetPairFormat(assetType asset.Item, requestFormat bool) (currency.PairFormat, error) { - if b.CurrencyPairs.UseGlobalFormat { - if requestFormat { - if b.CurrencyPairs.RequestFormat == nil { - return currency.EMPTYFORMAT, errGlobalRequestFormatIsNil - } - return *b.CurrencyPairs.RequestFormat, nil - } - - if b.CurrencyPairs.ConfigFormat == nil { - return currency.EMPTYFORMAT, errGlobalConfigFormatIsNil - } - return *b.CurrencyPairs.ConfigFormat, nil - } - - ps, err := b.CurrencyPairs.Get(assetType) - if err != nil { - return currency.EMPTYFORMAT, err - } - - if requestFormat { - if ps.RequestFormat == nil { - return currency.EMPTYFORMAT, errAssetRequestFormatIsNil - } - return *ps.RequestFormat, nil - } - - if ps.ConfigFormat == nil { - return currency.EMPTYFORMAT, errAssetConfigFormatIsNil - } - return *ps.ConfigFormat, nil +// GetPairFormat returns the pair format based on the exchange and asset type +func (b *Base) GetPairFormat(a asset.Item, r bool) (currency.PairFormat, error) { + return b.CurrencyPairs.GetFormat(a, r) } // GetEnabledPairs is a method that returns the enabled currency pairs of @@ -1007,11 +973,9 @@ func (b *Base) FormatWithdrawPermissions() string { return NoAPIWithdrawalMethodsText } -// SupportsAsset whether or not the supplied asset is supported -// by the exchange +// SupportsAsset whether or not the supplied asset is supported by the exchange func (b *Base) SupportsAsset(a asset.Item) bool { - _, ok := b.CurrencyPairs.Pairs[a] - return ok + return b.CurrencyPairs.IsAssetSupported(a) } // PrintEnabledPairs prints the exchanges enabled asset pairs @@ -1082,12 +1046,10 @@ func (b *Base) StoreAssetPairFormat(a asset.Item, f currency.PairStore) error { return nil } -// SetGlobalPairsManager sets defined asset and pairs management system with -// global formatting +// SetGlobalPairsManager sets defined asset and pairs management system with global formatting func (b *Base) SetGlobalPairsManager(request, config *currency.PairFormat, assets ...asset.Item) error { if request == nil { - return fmt.Errorf("%s cannot set pairs manager, request pair format not provided", - b.Name) + return fmt.Errorf("%s cannot set pairs manager, request pair format not provided", b.Name) } if config == nil { @@ -1119,10 +1081,10 @@ func (b *Base) SetGlobalPairsManager(request, config *currency.PairFormat, asset for i := range assets { if assets[i].String() == "" { b.CurrencyPairs.Pairs = nil - return fmt.Errorf("%s cannot set pairs manager, asset is empty string", - b.Name) + return fmt.Errorf("%s cannot set pairs manager, asset is empty string", b.Name) } b.CurrencyPairs.Pairs[assets[i]] = new(currency.PairStore) + b.CurrencyPairs.Pairs[assets[i]].AssetEnabled = convert.BoolPtr(true) b.CurrencyPairs.Pairs[assets[i]].ConfigFormat = config b.CurrencyPairs.Pairs[assets[i]].RequestFormat = request } diff --git a/exchanges/exchange_test.go b/exchanges/exchange_test.go index e7d9cc9c..b6cd3d2c 100644 --- a/exchanges/exchange_test.go +++ b/exchanges/exchange_test.go @@ -696,56 +696,12 @@ func TestGetFeatures(t *testing.T) { } } +// TestGetPairFormat ensures that GetPairFormat delegates to PairsManager.GetFormat func TestGetPairFormat(t *testing.T) { t.Parallel() - // Test global formatting - var b Base - b.CurrencyPairs.UseGlobalFormat = true - b.CurrencyPairs.ConfigFormat = ¤cy.PairFormat{ - Uppercase: true, - } - b.CurrencyPairs.RequestFormat = ¤cy.PairFormat{ - Delimiter: "~", - } - pFmt, err := b.GetPairFormat(asset.Spot, true) - if err != nil { - t.Fatal(err) - } - if pFmt.Delimiter != "~" && !pFmt.Uppercase { - t.Error("incorrect pair format values") - } - pFmt, err = b.GetPairFormat(asset.Spot, false) - if err != nil { - t.Fatal(err) - } - if pFmt.Delimiter != "" && pFmt.Uppercase { - t.Error("incorrect pair format values") - } - - // Test individual asset pair store formatting - b.CurrencyPairs.UseGlobalFormat = false - 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) - } - if pFmt.Delimiter != "" && pFmt.Uppercase { - t.Error("incorrect pair format values") - } - pFmt, err = b.GetPairFormat(asset.Spot, true) - if err != nil { - t.Fatal(err) - } - if pFmt.Delimiter != "~" && !pFmt.Uppercase { - t.Error("incorrect pair format values") - } + _, err := new(Base).GetPairFormat(asset.Spot, true) + require.ErrorIs(t, err, asset.ErrNotSupported, "Must delegate to GetFormat and error") } func TestGetPairs(t *testing.T) { @@ -778,6 +734,7 @@ func TestGetPairs(t *testing.T) { } } +// TestFormatExchangeCurrencies exercises FormatExchangeCurrencies func TestFormatExchangeCurrencies(t *testing.T) { t.Parallel() @@ -797,32 +754,18 @@ func TestFormatExchangeCurrencies(t *testing.T) { }, }, } - p1, err := currency.NewPairDelimiter("BTC_USD", "_") - if err != nil { - t.Fatal(err) - } - p2, err := currency.NewPairDelimiter("LTC_BTC", "_") - if err != nil { - t.Fatal(err) - } + var pairs = []currency.Pair{ - p1, - p2, + currency.NewPairWithDelimiter("BTC", "USD", "_"), + currency.NewPairWithDelimiter("LTC", "BTC", "_"), } - actual, err := e.FormatExchangeCurrencies(pairs, asset.Spot) - if err != nil { - t.Errorf("Exchange TestFormatExchangeCurrencies error %s", err) - } - if expected := "btc~usd^ltc~btc"; actual != expected { - t.Errorf("Exchange TestFormatExchangeCurrencies %s != %s", - actual, expected) - } + got, err := e.FormatExchangeCurrencies(pairs, asset.Spot) + require.NoError(t, err) + assert.Equal(t, "btc~usd^ltc~btc", got) _, err = e.FormatExchangeCurrencies(nil, asset.Spot) - if err == nil { - t.Error("nil pairs should return an error") - } + assert.ErrorContains(t, err, "returned empty string", err, "FormatExchangeCurrencies should error correctly") } func TestFormatExchangeCurrency(t *testing.T) { @@ -1342,14 +1285,12 @@ func TestSupportsAsset(t *testing.T) { t.Parallel() var b Base b.CurrencyPairs.Pairs = map[asset.Item]*currency.PairStore{ - asset.Spot: {}, - } - if !b.SupportsAsset(asset.Spot) { - t.Error("spot should be supported") - } - if b.SupportsAsset(asset.Index) { - t.Error("index shouldn't be supported") + asset.Spot: { + AssetEnabled: convert.BoolPtr(true), + }, } + assert.True(t, b.SupportsAsset(asset.Spot), "Spot should be supported") + assert.False(t, b.SupportsAsset(asset.Index), "Index should not be supported") } func TestPrintEnabledPairs(t *testing.T) { @@ -1494,58 +1435,36 @@ func TestStoreAssetPairFormat(t *testing.T) { } func TestSetGlobalPairsManager(t *testing.T) { - b := Base{ - Config: &config.Exchange{Name: "kitties"}, - } + b := Base{Config: &config.Exchange{Name: "kitties"}} err := b.SetGlobalPairsManager(nil, nil, asset.Empty) - if err == nil { - t.Error("error cannot be nil") - } + assert.ErrorContains(t, err, "cannot set pairs manager, request pair format not provided") err = b.SetGlobalPairsManager(¤cy.PairFormat{Uppercase: true}, nil, asset.Empty) - if err == nil { - t.Error("error cannot be nil") - } + assert.ErrorContains(t, err, "cannot set pairs manager, config pair format not provided") - err = b.SetGlobalPairsManager(¤cy.PairFormat{Uppercase: true}, - ¤cy.PairFormat{Uppercase: true}) - if err == nil { - t.Error("error cannot be nil") - } + err = b.SetGlobalPairsManager(¤cy.PairFormat{Uppercase: true}, ¤cy.PairFormat{Uppercase: true}) + assert.ErrorContains(t, err, " cannot set pairs manager, no assets provided") - err = b.SetGlobalPairsManager(¤cy.PairFormat{Uppercase: true}, - ¤cy.PairFormat{Uppercase: true}, asset.Empty) - if err == nil { - t.Error("error cannot be nil") - } + err = b.SetGlobalPairsManager(¤cy.PairFormat{Uppercase: true}, ¤cy.PairFormat{Uppercase: true}, asset.Empty) + assert.ErrorContains(t, err, " cannot set global pairs manager config pair format requires delimiter for assets") err = b.SetGlobalPairsManager(¤cy.PairFormat{Uppercase: true}, ¤cy.PairFormat{Uppercase: true}, asset.Spot, asset.Binary) - if !errors.Is(err, errConfigPairFormatRequiresDelimiter) { - t.Fatalf("received: '%v' but expected: '%v'", err, errConfigPairFormatRequiresDelimiter) - } + assert.ErrorIs(t, 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) - } + err = b.SetGlobalPairsManager(¤cy.PairFormat{Uppercase: true}, ¤cy.PairFormat{Uppercase: true, Delimiter: currency.DashDelimiter}, asset.Spot, asset.Binary) + require.NoError(t, err, "SetGlobalPairsManager must not error") - if !b.SupportsAsset(asset.Binary) || !b.SupportsAsset(asset.Spot) { - t.Fatal("global pairs manager not set correctly") - } + assert.True(t, b.SupportsAsset(asset.Binary), "Pairs Manager must support Binary") + assert.True(t, b.SupportsAsset(asset.Spot), "Pairs Manager must support Spot") - err = b.SetGlobalPairsManager(¤cy.PairFormat{Uppercase: true}, - ¤cy.PairFormat{Uppercase: true}, asset.Spot, asset.Binary) - if err == nil { - t.Error("error cannot be nil") - } + err = b.SetGlobalPairsManager(¤cy.PairFormat{Uppercase: true}, ¤cy.PairFormat{Uppercase: true}, asset.Spot, asset.Binary) + assert.ErrorIs(t, err, errConfigPairFormatRequiresDelimiter, "SetGlobalPairsManager should error correctly") } + func Test_FormatExchangeKlineInterval(t *testing.T) { testCases := []struct { name string @@ -2366,9 +2285,7 @@ func TestGetKlineRequest(t *testing.T) { b.Features.Enabled.Kline.Intervals = kline.DeployExchangeIntervals(kline.IntervalCapacity{Interval: kline.OneMin}) b.Features.Enabled.Kline.GlobalResultLimit = 1439 _, err = b.GetKlineRequest(pair, asset.Spot, kline.OneHour, time.Time{}, time.Time{}, false) - if !errors.Is(err, errAssetRequestFormatIsNil) { - t.Fatalf("received: '%v' but expected: '%v'", err, errAssetRequestFormatIsNil) - } + assert.ErrorIs(t, err, currency.ErrPairFormatIsNil, "GetKlineRequest should return Format is Nil") err = b.CurrencyPairs.Store(asset.Spot, ¤cy.PairStore{ AssetEnabled: convert.BoolPtr(true), @@ -2533,9 +2450,7 @@ func TestGetKlineExtendedRequest(t *testing.T) { } _, err = b.GetKlineExtendedRequest(pair, asset.Spot, kline.OneHour, start, end) - if !errors.Is(err, errAssetRequestFormatIsNil) { - t.Fatalf("received: '%v' but expected: '%v'", err, errAssetRequestFormatIsNil) - } + assert.ErrorIs(t, err, currency.ErrPairFormatIsNil, "GetKlineExtendedRequest should error correctly") err = b.CurrencyPairs.Store(asset.Spot, ¤cy.PairStore{ AssetEnabled: convert.BoolPtr(true), diff --git a/exchanges/gateio/gateio.go b/exchanges/gateio/gateio.go index 4c500a75..7164f692 100644 --- a/exchanges/gateio/gateio.go +++ b/exchanges/gateio/gateio.go @@ -3753,8 +3753,12 @@ func (g *Gateio) IsValidPairString(currencyPair string) bool { if len(currencyPair) < 3 { return false } - if strings.Contains(currencyPair, g.CurrencyPairs.RequestFormat.Delimiter) { - result := strings.Split(currencyPair, g.CurrencyPairs.RequestFormat.Delimiter) + pf, err := g.CurrencyPairs.GetFormat(asset.Spot, true) + if err != nil { + return false + } + if strings.Contains(currencyPair, pf.Delimiter) { + result := strings.Split(currencyPair, pf.Delimiter) return len(result) >= 2 } return false diff --git a/exchanges/kraken/kraken_test.go b/exchanges/kraken/kraken_test.go index 44472ffb..8fd64157 100644 --- a/exchanges/kraken/kraken_test.go +++ b/exchanges/kraken/kraken_test.go @@ -130,31 +130,28 @@ func TestUpdateTicker(t *testing.T) { func TestUpdateTickers(t *testing.T) { t.Parallel() + k := new(Kraken) //nolint:govet // Intentional shadow to avoid future copy/paste mistakes + require.NoError(t, testexch.Setup(k), "Test instance Setup must not error") testexch.UpdatePairsOnce(t, k) + err := k.UpdateTickers(context.Background(), asset.Spot) + require.NoError(t, err, "UpdateTickers must not error") ap, err := k.GetAvailablePairs(asset.Spot) - require.NoError(t, err) - err = k.CurrencyPairs.StorePairs(asset.Spot, ap, true) - require.NoError(t, err) - err = k.UpdateTickers(context.Background(), asset.Spot) - assert.NoError(t, err) + require.NoError(t, err, "GetAvailablePairs must not error") for i := range ap { _, err = ticker.GetTicker(k.Name, ap[i], asset.Spot) - assert.NoError(t, err) + require.NoError(t, err, "GetTicker must not error") } - ap, err = k.GetAvailablePairs(asset.Futures) - require.NoError(t, err) - err = k.CurrencyPairs.StorePairs(asset.Futures, ap, true) - require.NoError(t, err) + require.NoError(t, err, "GetAvailablePairs must not error") err = k.UpdateTickers(context.Background(), asset.Futures) - assert.NoError(t, err) + require.NoError(t, err, "UpdateTickers must not error") for i := range ap { _, err = ticker.GetTicker(k.Name, ap[i], asset.Futures) - assert.NoError(t, err) + require.NoError(t, err, "GetTicker must not error") } err = k.UpdateTickers(context.Background(), asset.Index) - assert.ErrorIs(t, err, asset.ErrNotSupported) + assert.ErrorIs(t, err, asset.ErrNotSupported, "UpdateTickers should error correctly on Index asset") } func TestUpdateOrderbook(t *testing.T) { @@ -1827,73 +1824,68 @@ func TestWsOwnTrades(t *testing.T) { func TestWsOpenOrders(t *testing.T) { t.Parallel() - n := new(Kraken) + k := new(Kraken) //nolint:govet // Intentional shadow to avoid future copy/paste mistakes + require.NoError(t, testexch.Setup(k), "Test instance Setup must not error") testexch.UpdatePairsOnce(t, k) - sharedtestvalues.TestFixtureToDataHandler(t, k, n, "testdata/wsOpenTrades.json", n.wsHandleData) - seen := 0 - for reading := true; reading; { - select { - default: - reading = false - case resp := <-n.Websocket.DataHandler: - seen++ - switch v := resp.(type) { - case *order.Detail: - switch seen { - case 1: - assert.Equal(t, "OGTT3Y-C6I3P-XRI6HR", v.OrderID, "OrderID") - assert.Equal(t, order.Limit, v.Type, "order type") - assert.Equal(t, order.Sell, v.Side, "order side") - assert.Equal(t, order.Open, v.Status, "order status") - assert.Equal(t, 34.5, v.Price, "price") - assert.Equal(t, 10.00345345, v.Amount, "amount") - case 2: - assert.Equal(t, "OKB55A-UEMMN-YUXM2A", v.OrderID, "OrderID") - assert.Equal(t, order.Market, v.Type, "order type") - assert.Equal(t, order.Buy, v.Side, "order side") - assert.Equal(t, order.Pending, v.Status, "order status") - assert.Equal(t, 0.0, v.Price, "price") - assert.Equal(t, 0.0001, v.Amount, "amount") - assert.Equal(t, time.UnixMicro(1692851641361371), v.Date, "Date") - case 3: - assert.Equal(t, "OKB55A-UEMMN-YUXM2A", v.OrderID, "OrderID") - assert.Equal(t, order.Open, v.Status, "order status") - case 4: - assert.Equal(t, "OKB55A-UEMMN-YUXM2A", v.OrderID, "OrderID") - assert.Equal(t, order.UnknownStatus, v.Status, "order status") - assert.Equal(t, 26425.2, v.AverageExecutedPrice, "AverageExecutedPrice") - assert.Equal(t, 0.0001, v.ExecutedAmount, "ExecutedAmount") - assert.Equal(t, 0.0, v.RemainingAmount, "RemainingAmount") // Not in the message; Testing regression to bad derivation - assert.Equal(t, 0.00687, v.Fee, "Fee") - case 5: - assert.Equal(t, "OKB55A-UEMMN-YUXM2A", v.OrderID, "OrderID") - assert.Equal(t, order.Closed, v.Status, "order status") - assert.Equal(t, 0.0001, v.ExecutedAmount, "ExecutedAmount") - assert.Equal(t, 26425.2, v.AverageExecutedPrice, "AverageExecutedPrice") - assert.Equal(t, 0.00687, v.Fee, "Fee") - assert.Equal(t, time.UnixMicro(1692851641361447), v.LastUpdated, "LastUpdated") - case 6: - assert.Equal(t, "OGTT3Y-C6I3P-XRI6HR", v.OrderID, "OrderID") - assert.Equal(t, order.UnknownStatus, v.Status, "order status") - assert.Equal(t, 10.00345345, v.ExecutedAmount, "ExecutedAmount") - assert.Equal(t, 0.001, v.Fee, "Fee") - assert.Equal(t, 34.5, v.AverageExecutedPrice, "AverageExecutedPrice") - case 7: - assert.Equal(t, "OGTT3Y-C6I3P-XRI6HR", v.OrderID, "OrderID") - assert.Equal(t, order.Closed, v.Status, "order status") - assert.Equal(t, time.UnixMicro(1692675961789052), v.LastUpdated, "LastUpdated") - assert.Equal(t, 10.00345345, v.ExecutedAmount, "ExecutedAmount") - assert.Equal(t, 0.001, v.Fee, "Fee") - assert.Equal(t, 34.5, v.AverageExecutedPrice, "AverageExecutedPrice") - reading = false - } - default: - t.Errorf("Unexpected type in DataHandler: %T (%s)", v, v) + testexch.FixtureToDataHandler(t, "testdata/wsOpenTrades.json", k.wsHandleData) + close(k.Websocket.DataHandler) + assert.Len(t, k.Websocket.DataHandler, 7, "Should see 7 orders") + for resp := range k.Websocket.DataHandler { + switch v := resp.(type) { + case *order.Detail: + switch len(k.Websocket.DataHandler) { + case 6: + assert.Equal(t, "OGTT3Y-C6I3P-XRI6HR", v.OrderID, "OrderID") + assert.Equal(t, order.Limit, v.Type, "order type") + assert.Equal(t, order.Sell, v.Side, "order side") + assert.Equal(t, order.Open, v.Status, "order status") + assert.Equal(t, 34.5, v.Price, "price") + assert.Equal(t, 10.00345345, v.Amount, "amount") + case 5: + assert.Equal(t, "OKB55A-UEMMN-YUXM2A", v.OrderID, "OrderID") + assert.Equal(t, order.Market, v.Type, "order type") + assert.Equal(t, order.Buy, v.Side, "order side") + assert.Equal(t, order.Pending, v.Status, "order status") + assert.Equal(t, 0.0, v.Price, "price") + assert.Equal(t, 0.0001, v.Amount, "amount") + assert.Equal(t, time.UnixMicro(1692851641361371), v.Date, "Date") + case 4: + assert.Equal(t, "OKB55A-UEMMN-YUXM2A", v.OrderID, "OrderID") + assert.Equal(t, order.Open, v.Status, "order status") + case 3: + assert.Equal(t, "OKB55A-UEMMN-YUXM2A", v.OrderID, "OrderID") + assert.Equal(t, order.UnknownStatus, v.Status, "order status") + assert.Equal(t, 26425.2, v.AverageExecutedPrice, "AverageExecutedPrice") + assert.Equal(t, 0.0001, v.ExecutedAmount, "ExecutedAmount") + assert.Equal(t, 0.0, v.RemainingAmount, "RemainingAmount") // Not in the message; Testing regression to bad derivation + assert.Equal(t, 0.00687, v.Fee, "Fee") + case 2: + assert.Equal(t, "OKB55A-UEMMN-YUXM2A", v.OrderID, "OrderID") + assert.Equal(t, order.Closed, v.Status, "order status") + assert.Equal(t, 0.0001, v.ExecutedAmount, "ExecutedAmount") + assert.Equal(t, 26425.2, v.AverageExecutedPrice, "AverageExecutedPrice") + assert.Equal(t, 0.00687, v.Fee, "Fee") + assert.Equal(t, time.UnixMicro(1692851641361447), v.LastUpdated, "LastUpdated") + case 1: + assert.Equal(t, "OGTT3Y-C6I3P-XRI6HR", v.OrderID, "OrderID") + assert.Equal(t, order.UnknownStatus, v.Status, "order status") + assert.Equal(t, 10.00345345, v.ExecutedAmount, "ExecutedAmount") + assert.Equal(t, 0.001, v.Fee, "Fee") + assert.Equal(t, 34.5, v.AverageExecutedPrice, "AverageExecutedPrice") + case 0: + assert.Equal(t, "OGTT3Y-C6I3P-XRI6HR", v.OrderID, "OrderID") + assert.Equal(t, order.Closed, v.Status, "order status") + assert.Equal(t, time.UnixMicro(1692675961789052), v.LastUpdated, "LastUpdated") + assert.Equal(t, 10.00345345, v.ExecutedAmount, "ExecutedAmount") + assert.Equal(t, 0.001, v.Fee, "Fee") + assert.Equal(t, 34.5, v.AverageExecutedPrice, "AverageExecutedPrice") } + case error: + t.Error(v) + default: + t.Errorf("Unexpected type in DataHandler: %T (%s)", v, v) } } - - assert.Equal(t, 7, seen, "number of DataHandler emissions") } func TestWsAddOrderJSON(t *testing.T) { diff --git a/exchanges/kucoin/kucoin_test.go b/exchanges/kucoin/kucoin_test.go index d43cb821..8d7d9a7a 100644 --- a/exchanges/kucoin/kucoin_test.go +++ b/exchanges/kucoin/kucoin_test.go @@ -1988,8 +1988,9 @@ func TestGetAuthenticatedServersInstances(t *testing.T) { } func TestPushData(t *testing.T) { - n := new(Kucoin) - sharedtestvalues.TestFixtureToDataHandler(t, ku, n, "testdata/wsHandleData.json", ku.wsHandleData) + t.Parallel() + ku := testInstance(t) //nolint:govet // Intentional shadow to avoid future copy/paste mistakes + testexch.FixtureToDataHandler(t, "testdata/wsHandleData.json", ku.wsHandleData) } func verifySubs(tb testing.TB, subs []subscription.Subscription, a asset.Item, prefix string, expected ...string) { @@ -2049,14 +2050,10 @@ func TestGenerateDefaultSubscriptions(t *testing.T) { func TestGenerateAuthSubscriptions(t *testing.T) { t.Parallel() - // Create a parallel safe Kucoin to mess with - nu := new(Kucoin) - nu.Base.Features = ku.Base.Features - assert.NoError(t, nu.CurrencyPairs.Load(&ku.CurrencyPairs), "Loading Pairs should not error") - nu.Websocket = sharedtestvalues.NewTestWebsocket() - nu.Websocket.SetCanUseAuthenticatedEndpoints(true) + ku := testInstance(t) //nolint:govet // Intentional shadow to avoid future copy/paste mistakes + ku.Websocket.SetCanUseAuthenticatedEndpoints(true) - subs, err := nu.GenerateDefaultSubscriptions() + subs, err := ku.GenerateDefaultSubscriptions() assert.NoError(t, err, "GenerateDefaultSubscriptions with Auth should not error") assert.Len(t, subs, 24, "Should generate the correct number of subs when logged in") @@ -2086,17 +2083,12 @@ func TestGenerateAuthSubscriptions(t *testing.T) { func TestGenerateCandleSubscription(t *testing.T) { t.Parallel() - // Create a parallel safe Kucoin to mess with - nu := new(Kucoin) - nu.Base.Features = ku.Base.Features - nu.Websocket = sharedtestvalues.NewTestWebsocket() - assert.NoError(t, nu.CurrencyPairs.Load(&ku.CurrencyPairs), "Loading Pairs should not error") - - nu.Features.Subscriptions = []*subscription.Subscription{ + ku := testInstance(t) //nolint:govet // Intentional shadow to avoid future copy/paste mistakes + ku.Features.Subscriptions = []*subscription.Subscription{ {Channel: subscription.CandlesChannel, Interval: kline.FourHour}, } - subs, err := nu.GenerateDefaultSubscriptions() + subs, err := ku.GenerateDefaultSubscriptions() assert.NoError(t, err, "GenerateDefaultSubscriptions with Candles should not error") assert.Len(t, subs, 6, "Should generate the correct number of subs for candles") @@ -2111,17 +2103,12 @@ func TestGenerateCandleSubscription(t *testing.T) { func TestGenerateMarketSubscription(t *testing.T) { t.Parallel() - // Create a parallel safe Kucoin to mess with - nu := new(Kucoin) - nu.Base.Features = ku.Base.Features - nu.Websocket = sharedtestvalues.NewTestWebsocket() - assert.NoError(t, nu.CurrencyPairs.Load(&ku.CurrencyPairs), "Loading Pairs should not error") - - nu.Features.Subscriptions = []*subscription.Subscription{ + ku := testInstance(t) //nolint:govet // Intentional shadow to avoid future copy/paste mistakes + ku.Features.Subscriptions = []*subscription.Subscription{ {Channel: marketSnapshotChannel}, } - subs, err := nu.GenerateDefaultSubscriptions() + subs, err := ku.GenerateDefaultSubscriptions() assert.NoError(t, err, "GenerateDefaultSubscriptions with MarketSnapshot should not error") assert.Len(t, subs, 7, "Should generate the correct number of subs for snapshot") @@ -2490,59 +2477,51 @@ func TestProcessOrderbook(t *testing.T) { func TestProcessMarketSnapshot(t *testing.T) { t.Parallel() - n := new(Kucoin) - sharedtestvalues.TestFixtureToDataHandler(t, ku, n, "testdata/wsMarketSnapshot.json", n.wsHandleData) - seen := 0 + ku := testInstance(t) //nolint:govet // Intentional shadow to avoid future copy/paste mistakes + testexch.FixtureToDataHandler(t, "testdata/wsMarketSnapshot.json", ku.wsHandleData) + close(ku.Websocket.DataHandler) + assert.Len(t, ku.Websocket.DataHandler, 4, "Should see 4 tickers") seenAssetTypes := map[asset.Item]int{} - for reading := true; reading; { - select { - default: - reading = false - case resp := <-n.GetBase().Websocket.DataHandler: - seen++ - switch v := resp.(type) { - case *ticker.Price: - switch seen { - case 1: - assert.Equal(t, asset.Margin, v.AssetType, "AssetType") - assert.Equal(t, time.UnixMilli(1700555342007), v.LastUpdated, "datetime") - assert.Equal(t, 0.004445, v.High, "high") - assert.Equal(t, 0.004415, v.Last, "lastTradedPrice") - assert.Equal(t, 0.004191, v.Low, "low") - assert.Equal(t, currency.NewPairWithDelimiter("TRX", "BTC", "-"), v.Pair, "symbol") - assert.Equal(t, 13097.3357, v.Volume, "volume") - assert.Equal(t, 57.44552981, v.QuoteVolume, "volValue") - case 2, 3: - assert.Equal(t, time.UnixMilli(1700555340197), v.LastUpdated, "datetime") - assert.Contains(t, []asset.Item{asset.Spot, asset.Margin}, v.AssetType, "AssetType is Spot or Margin") - seenAssetTypes[v.AssetType]++ - assert.Equal(t, 1, seenAssetTypes[v.AssetType], "Each Asset Type is sent only once per unique snapshot") - assert.Equal(t, 0.054846, v.High, "high") - assert.Equal(t, 0.053778, v.Last, "lastTradedPrice") - assert.Equal(t, 0.05364, v.Low, "low") - assert.Equal(t, currency.NewPairWithDelimiter("ETH", "BTC", "-"), v.Pair, "symbol") - assert.Equal(t, 2958.3139116, v.Volume, "volume") - assert.Equal(t, 160.7847672784213, v.QuoteVolume, "volValue") - case 4: - assert.Equal(t, asset.Spot, v.AssetType, "AssetType") - assert.Equal(t, time.UnixMilli(1700555342151), v.LastUpdated, "datetime") - assert.Equal(t, 37750.0, v.High, "high") - assert.Equal(t, 37366.8, v.Last, "lastTradedPrice") - assert.Equal(t, 36700.0, v.Low, "low") - assert.Equal(t, currency.NewPairWithDelimiter("BTC", "USDT", "-"), v.Pair, "symbol") - assert.Equal(t, 2900.37846402, v.Volume, "volume") - assert.Equal(t, 108210331.34015164, v.QuoteVolume, "volValue") - default: - t.Errorf("Got an unexpected *ticker.Price: %v", v) - } - case error: - t.Error(v) - default: - t.Errorf("Got unexpected data: %T %v", v, v) + for resp := range ku.Websocket.DataHandler { + switch v := resp.(type) { + case *ticker.Price: + switch len(ku.Websocket.DataHandler) { + case 3: + assert.Equal(t, asset.Margin, v.AssetType, "AssetType") + assert.Equal(t, time.UnixMilli(1700555342007), v.LastUpdated, "datetime") + assert.Equal(t, 0.004445, v.High, "high") + assert.Equal(t, 0.004415, v.Last, "lastTradedPrice") + assert.Equal(t, 0.004191, v.Low, "low") + assert.Equal(t, currency.NewPairWithDelimiter("TRX", "BTC", "-"), v.Pair, "symbol") + assert.Equal(t, 13097.3357, v.Volume, "volume") + assert.Equal(t, 57.44552981, v.QuoteVolume, "volValue") + case 2, 1: + assert.Equal(t, time.UnixMilli(1700555340197), v.LastUpdated, "datetime") + assert.Contains(t, []asset.Item{asset.Spot, asset.Margin}, v.AssetType, "AssetType is Spot or Margin") + seenAssetTypes[v.AssetType]++ + assert.Equal(t, 1, seenAssetTypes[v.AssetType], "Each Asset Type is sent only once per unique snapshot") + assert.Equal(t, 0.054846, v.High, "high") + assert.Equal(t, 0.053778, v.Last, "lastTradedPrice") + assert.Equal(t, 0.05364, v.Low, "low") + assert.Equal(t, currency.NewPairWithDelimiter("ETH", "BTC", "-"), v.Pair, "symbol") + assert.Equal(t, 2958.3139116, v.Volume, "volume") + assert.Equal(t, 160.7847672784213, v.QuoteVolume, "volValue") + case 0: + assert.Equal(t, asset.Spot, v.AssetType, "AssetType") + assert.Equal(t, time.UnixMilli(1700555342151), v.LastUpdated, "datetime") + assert.Equal(t, 37750.0, v.High, "high") + assert.Equal(t, 37366.8, v.Last, "lastTradedPrice") + assert.Equal(t, 36700.0, v.Low, "low") + assert.Equal(t, currency.NewPairWithDelimiter("BTC", "USDT", "-"), v.Pair, "symbol") + assert.Equal(t, 2900.37846402, v.Volume, "volume") + assert.Equal(t, 108210331.34015164, v.QuoteVolume, "volValue") } + case error: + t.Error(v) + default: + t.Errorf("Got unexpected data: %T %v", v, v) } } - assert.Equal(t, 4, seen, "Number of messages") } func TestSubscribeMarketSnapshot(t *testing.T) { @@ -2735,16 +2714,16 @@ func TestUpdateOrderExecutionLimits(t *testing.T) { func TestGetOpenInterest(t *testing.T) { t.Parallel() - nu := new(Kucoin) - require.NoError(t, testexch.Setup(nu), "Test exchange Setup must not error") - _, err := nu.GetOpenInterest(context.Background(), key.PairAsset{ + ku := testInstance(t) //nolint:govet // Intentional shadow to avoid future copy/paste mistakes + + _, err := ku.GetOpenInterest(context.Background(), key.PairAsset{ Base: currency.ETH.Item, Quote: currency.USDT.Item, Asset: asset.USDTMarginedFutures, }) assert.ErrorIs(t, err, asset.ErrNotSupported) - resp, err := nu.GetOpenInterest(context.Background(), key.PairAsset{ + resp, err := ku.GetOpenInterest(context.Background(), key.PairAsset{ Base: futuresTradablePair.Base.Item, Quote: futuresTradablePair.Quote.Item, Asset: asset.Futures, @@ -2753,8 +2732,8 @@ func TestGetOpenInterest(t *testing.T) { assert.NotEmpty(t, resp) cp1 := currency.NewPair(currency.ETH, currency.USDTM) - sharedtestvalues.SetupCurrencyPairsForExchangeAsset(t, nu, asset.Futures, cp1) - resp, err = nu.GetOpenInterest(context.Background(), + sharedtestvalues.SetupCurrencyPairsForExchangeAsset(t, ku, asset.Futures, cp1) + resp, err = ku.GetOpenInterest(context.Background(), key.PairAsset{ Base: futuresTradablePair.Base.Item, Quote: futuresTradablePair.Quote.Item, @@ -2769,7 +2748,7 @@ func TestGetOpenInterest(t *testing.T) { assert.NoError(t, err) assert.NotEmpty(t, resp) - resp, err = nu.GetOpenInterest(context.Background()) + resp, err = ku.GetOpenInterest(context.Background()) assert.NoError(t, err) assert.NotEmpty(t, resp) } @@ -2786,3 +2765,15 @@ func TestGetCurrencyTradeURL(t *testing.T) { assert.NotEmpty(t, resp) } } + +// testInstance returns a local Kucoin for isolated testing +func testInstance(tb testing.TB) *Kucoin { + tb.Helper() + ku := new(Kucoin) + require.NoError(tb, testexch.Setup(ku), "Test instance Setup must not error") + ku.obm = &orderbookManager{ + state: make(map[currency.Code]map[currency.Code]map[asset.Item]*update), + jobs: make(chan job, maxWSOrderbookJobs), + } + return ku +} diff --git a/exchanges/okx/okx.go b/exchanges/okx/okx.go index 3322c504..2e951367 100644 --- a/exchanges/okx/okx.go +++ b/exchanges/okx/okx.go @@ -3065,15 +3065,6 @@ func (ok *Okx) GetUnderlying(pair currency.Pair, a asset.Item) (string, error) { return pair.Base.String() + format.Delimiter + pair.Quote.String(), nil } -// GetPairFromInstrumentID returns a currency pair give an instrument ID and asset Item, which represents the instrument type. -func (ok *Okx) GetPairFromInstrumentID(instrumentID string) (currency.Pair, error) { - codes := strings.Split(instrumentID, ok.CurrencyPairs.RequestFormat.Delimiter) - if len(codes) >= 2 { - instrumentID = codes[0] + ok.CurrencyPairs.RequestFormat.Delimiter + strings.Join(codes[1:], ok.CurrencyPairs.RequestFormat.Delimiter) - } - return currency.NewPairFromString(instrumentID) -} - // GetOrderBookDepth returns the recent order asks and bids before specified timestamp. func (ok *Okx) GetOrderBookDepth(ctx context.Context, instrumentID string, depth int64) (*OrderBookResponse, error) { params := url.Values{} @@ -4320,11 +4311,15 @@ func (ok *Okx) GetAssetsFromInstrumentTypeOrID(instType, instrumentID string) ([ if instrumentID == "" { return nil, fmt.Errorf("%w instrumentID", errEmptyArgument) } - splitSymbol := strings.Split(instrumentID, ok.CurrencyPairs.RequestFormat.Delimiter) + pf, err := ok.CurrencyPairs.GetFormat(asset.Spot, true) + if err != nil { + return nil, err + } + splitSymbol := strings.Split(instrumentID, pf.Delimiter) if len(splitSymbol) <= 1 { return nil, fmt.Errorf("%w %v", currency.ErrCurrencyNotSupported, instrumentID) } - pair, err := currency.NewPairDelimiter(instrumentID, ok.CurrencyPairs.RequestFormat.Delimiter) + pair, err := currency.NewPairDelimiter(instrumentID, pf.Delimiter) if err != nil { return nil, err } diff --git a/exchanges/okx/okx_test.go b/exchanges/okx/okx_test.go index 24252aa1..37812082 100644 --- a/exchanges/okx/okx_test.go +++ b/exchanges/okx/okx_test.go @@ -2206,24 +2206,6 @@ func TestWithdraw(t *testing.T) { } } -func TestGetPairFromInstrumentID(t *testing.T) { - t.Parallel() - instruments := []string{ - "BTC-USDT", - "BTC-USDT-SWAP", - "BTC-USDT-ER33234", - } - if _, err := ok.GetPairFromInstrumentID(instruments[0]); err != nil { - t.Error("Okx GetPairFromInstrumentID() error", err) - } - if _, ere := ok.GetPairFromInstrumentID(instruments[1]); ere != nil { - t.Error("Okx GetPairFromInstrumentID() error", ere) - } - if _, erf := ok.GetPairFromInstrumentID(instruments[2]); erf != nil { - t.Error("Okx GetPairFromInstrumentID() error", erf) - } -} - func TestGetActiveOrders(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) @@ -2534,68 +2516,63 @@ func TestBalanceAndPosition(t *testing.T) { func TestOrderPushData(t *testing.T) { t.Parallel() - n := new(Okx) - sharedtestvalues.TestFixtureToDataHandler(t, ok, n, "testdata/wsOrders.json", n.WsHandleData) - seen := 0 - for reading := true; reading; { - select { - default: - reading = false - case resp := <-n.GetBase().Websocket.DataHandler: - seen++ - switch v := resp.(type) { - case *order.Detail: - switch seen { - case 1: - assert.Equal(t, "452197707845865472", v.OrderID, "OrderID") - assert.Equal(t, "HamsterParty14", v.ClientOrderID, "ClientOrderID") - assert.Equal(t, asset.Spot, v.AssetType, "AssetType") - assert.Equal(t, order.Sell, v.Side, "Side") - assert.Equal(t, order.Filled, v.Status, "Status") - assert.Equal(t, order.Limit, v.Type, "Type") - assert.Equal(t, currency.NewPairWithDelimiter("BTC", "USDT", "-"), v.Pair, "Pair") - assert.Equal(t, 31527.1, v.AverageExecutedPrice, "AverageExecutedPrice") - assert.Equal(t, time.UnixMilli(1654084334977), v.Date, "Date") - assert.Equal(t, time.UnixMilli(1654084353263), v.CloseTime, "CloseTime") - assert.Equal(t, 0.001, v.Amount, "Amount") - assert.Equal(t, 0.001, v.ExecutedAmount, "ExecutedAmount") - assert.Equal(t, 0.000, v.RemainingAmount, "RemainingAmount") - assert.Equal(t, 31527.1, v.Price, "Price") - assert.Equal(t, 0.02522168, v.Fee, "Fee") - assert.Equal(t, currency.USDT, v.FeeAsset, "FeeAsset") - case 2: - assert.Equal(t, "620258920632008725", v.OrderID, "OrderID") - assert.Equal(t, asset.Spot, v.AssetType, "AssetType") - assert.Equal(t, order.Market, v.Type, "Type") - assert.Equal(t, order.Sell, v.Side, "Side") - assert.Equal(t, order.Active, v.Status, "Status") - assert.Equal(t, 0.0, v.Amount, "Amount should be 0 for a market sell") - assert.Equal(t, 10.0, v.QuoteAmount, "QuoteAmount") - case 3: - assert.Equal(t, "620258920632008725", v.OrderID, "OrderID") - assert.Equal(t, 10.0, v.QuoteAmount, "QuoteAmount") - assert.Equal(t, 0.00038127046945832905, v.Amount, "Amount") - assert.Equal(t, 0.010000249968, v.Fee, "Fee") - assert.Equal(t, 0.0, v.RemainingAmount, "RemainingAmount") - assert.Equal(t, 0.00038128, v.ExecutedAmount, "ExecutedAmount") - assert.Equal(t, order.PartiallyFilled, v.Status, "Status") - case 4: - assert.Equal(t, "620258920632008725", v.OrderID, "OrderID") - assert.Equal(t, 10.0, v.QuoteAmount, "QuoteAmount") - assert.Equal(t, 0.010000249968, v.Fee, "Fee") - assert.Equal(t, 0.0, v.RemainingAmount, "RemainingAmount") - assert.Equal(t, 0.00038128, v.ExecutedAmount, "ExecutedAmount") - assert.Equal(t, 0.00038128, v.Amount, "Amount should be derived because order filled") - assert.Equal(t, order.Filled, v.Status, "Status") - } - case error: - t.Error(v) - default: - t.Errorf("Got unexpected data: %T %v", v, v) + ok := new(Okx) //nolint:govet // Intentional shadow to avoid future copy/paste mistakes + require.NoError(t, testexch.Setup(ok), "Test instance Setup must not error") + testexch.FixtureToDataHandler(t, "testdata/wsOrders.json", ok.WsHandleData) + close(ok.Websocket.DataHandler) + assert.Len(t, ok.Websocket.DataHandler, 4, "Should see 4 orders") + for resp := range ok.Websocket.DataHandler { + switch v := resp.(type) { + case *order.Detail: + switch len(ok.Websocket.DataHandler) { + case 3: + assert.Equal(t, "452197707845865472", v.OrderID, "OrderID") + assert.Equal(t, "HamsterParty14", v.ClientOrderID, "ClientOrderID") + assert.Equal(t, asset.Spot, v.AssetType, "AssetType") + assert.Equal(t, order.Sell, v.Side, "Side") + assert.Equal(t, order.Filled, v.Status, "Status") + assert.Equal(t, order.Limit, v.Type, "Type") + assert.Equal(t, currency.NewPairWithDelimiter("BTC", "USDT", "-"), v.Pair, "Pair") + assert.Equal(t, 31527.1, v.AverageExecutedPrice, "AverageExecutedPrice") + assert.Equal(t, time.UnixMilli(1654084334977), v.Date, "Date") + assert.Equal(t, time.UnixMilli(1654084353263), v.CloseTime, "CloseTime") + assert.Equal(t, 0.001, v.Amount, "Amount") + assert.Equal(t, 0.001, v.ExecutedAmount, "ExecutedAmount") + assert.Equal(t, 0.000, v.RemainingAmount, "RemainingAmount") + assert.Equal(t, 31527.1, v.Price, "Price") + assert.Equal(t, 0.02522168, v.Fee, "Fee") + assert.Equal(t, currency.USDT, v.FeeAsset, "FeeAsset") + case 2: + assert.Equal(t, "620258920632008725", v.OrderID, "OrderID") + assert.Equal(t, asset.Spot, v.AssetType, "AssetType") + assert.Equal(t, order.Market, v.Type, "Type") + assert.Equal(t, order.Sell, v.Side, "Side") + assert.Equal(t, order.Active, v.Status, "Status") + assert.Equal(t, 0.0, v.Amount, "Amount should be 0 for a market sell") + assert.Equal(t, 10.0, v.QuoteAmount, "QuoteAmount") + case 1: + assert.Equal(t, "620258920632008725", v.OrderID, "OrderID") + assert.Equal(t, 10.0, v.QuoteAmount, "QuoteAmount") + assert.Equal(t, 0.00038127046945832905, v.Amount, "Amount") + assert.Equal(t, 0.010000249968, v.Fee, "Fee") + assert.Equal(t, 0.0, v.RemainingAmount, "RemainingAmount") + assert.Equal(t, 0.00038128, v.ExecutedAmount, "ExecutedAmount") + assert.Equal(t, order.PartiallyFilled, v.Status, "Status") + case 0: + assert.Equal(t, "620258920632008725", v.OrderID, "OrderID") + assert.Equal(t, 10.0, v.QuoteAmount, "QuoteAmount") + assert.Equal(t, 0.010000249968, v.Fee, "Fee") + assert.Equal(t, 0.0, v.RemainingAmount, "RemainingAmount") + assert.Equal(t, 0.00038128, v.ExecutedAmount, "ExecutedAmount") + assert.Equal(t, 0.00038128, v.Amount, "Amount should be derived because order filled") + assert.Equal(t, order.Filled, v.Status, "Status") } + case error: + t.Error(v) + default: + t.Errorf("Got unexpected data: %T %v", v, v) } } - assert.Equal(t, 4, seen, "Saw 4 records") } const algoOrdersPushDataJSON = `{"arg": {"channel": "orders-algo","uid": "77982378738415879","instType": "FUTURES","instId": "BTC-USD-200329"},"data": [{"instType": "FUTURES","instId": "BTC-USD-200329","ordId": "312269865356374016","ccy": "BTC","algoId": "1234","px": "999","sz": "3","tdMode": "cross","tgtCcy": "","notionalUsd": "","ordType": "trigger","side": "buy","posSide": "long","state": "live","lever": "20","tpTriggerPx": "","tpTriggerPxType": "","tpOrdPx": "","slTriggerPx": "","slTriggerPxType": "","triggerPx": "99","triggerPxType": "last","ordPx": "12","actualSz": "","actualPx": "","tag": "adadadadad","actualSide": "","triggerTime": "1597026383085","cTime": "1597026383000"}]}` diff --git a/exchanges/okx/okx_websocket.go b/exchanges/okx/okx_websocket.go index 948ff952..29f5435a 100644 --- a/exchanges/okx/okx_websocket.go +++ b/exchanges/okx/okx_websocket.go @@ -688,7 +688,8 @@ func (ok *Okx) wsProcessIndexCandles(respRaw []byte) error { if len(response.Data) == 0 { return errNoCandlestickDataFound } - pair, err := ok.GetPairFromInstrumentID(response.Argument.InstrumentID) + + pair, err := currency.NewPairFromString(response.Argument.InstrumentID) if err != nil { return err } @@ -750,7 +751,7 @@ func (ok *Okx) wsProcessOrderbook5(data []byte) error { return err } - pair, err := ok.GetPairFromInstrumentID(resp.Argument.InstrumentID) + pair, err := currency.NewPairFromString(resp.Argument.InstrumentID) if err != nil { return err } @@ -809,13 +810,11 @@ func (ok *Okx) wsProcessOrderBooks(data []byte) error { response.Action != wsOrderbookSnapshot { return errors.New("invalid order book action") } - var pair currency.Pair - var assets []asset.Item - assets, err = ok.GetAssetsFromInstrumentTypeOrID(response.Argument.InstrumentType, response.Argument.InstrumentID) + assets, err := ok.GetAssetsFromInstrumentTypeOrID(response.Argument.InstrumentType, response.Argument.InstrumentID) if err != nil { return err } - pair, err = ok.GetPairFromInstrumentID(response.Argument.InstrumentID) + pair, err := currency.NewPairFromString(response.Argument.InstrumentID) if err != nil { return err } @@ -1062,8 +1061,7 @@ func (ok *Okx) wsProcessTrades(data []byte) error { } trades := make([]trade.Data, 0, len(response.Data)*len(assets)) for i := range response.Data { - var pair currency.Pair - pair, err = ok.GetPairFromInstrumentID(response.Data[i].InstrumentID) + pair, err := currency.NewPairFromString(response.Data[i].InstrumentID) if err != nil { return err } @@ -1086,7 +1084,6 @@ func (ok *Okx) wsProcessTrades(data []byte) error { // wsProcessOrders handles websocket order push data responses. func (ok *Okx) wsProcessOrders(respRaw []byte) error { var response WsOrderResponse - var pair currency.Pair err := json.Unmarshal(respRaw, &response) if err != nil { return err @@ -1109,7 +1106,7 @@ func (ok *Okx) wsProcessOrders(respRaw []byte) error { Err: err, } } - pair, err = ok.GetPairFromInstrumentID(response.Data[x].InstrumentID) + pair, err := currency.NewPairFromString(response.Data[x].InstrumentID) if err != nil { return err } @@ -1189,7 +1186,7 @@ func (ok *Okx) wsProcessCandles(respRaw []byte) error { if len(response.Data) == 0 { return errNoCandlestickDataFound } - pair, err := ok.GetPairFromInstrumentID(response.Argument.InstrumentID) + pair, err := currency.NewPairFromString(response.Argument.InstrumentID) if err != nil { return err } @@ -1260,8 +1257,7 @@ func (ok *Okx) wsProcessTickers(data []byte) error { if err != nil { return err } - var c currency.Pair - c, err = ok.GetPairFromInstrumentID(response.Data[i].InstrumentID) + c, err := currency.NewPairFromString(response.Data[i].InstrumentID) if err != nil { return err } diff --git a/exchanges/okx/okx_wrapper.go b/exchanges/okx/okx_wrapper.go index 1f1f4405..d454e30d 100644 --- a/exchanges/okx/okx_wrapper.go +++ b/exchanges/okx/okx_wrapper.go @@ -259,9 +259,13 @@ func (ok *Okx) FetchTradablePairs(ctx context.Context, a asset.Item) (currency.P if err != nil { return nil, err } + pf, err := ok.CurrencyPairs.GetFormat(a, false) + if err != nil { + return nil, err + } pairs := make([]currency.Pair, len(insts)) for x := range insts { - pairs[x], err = currency.NewPairDelimiter(insts[x].InstrumentID, ok.CurrencyPairs.ConfigFormat.Delimiter) + pairs[x], err = currency.NewPairDelimiter(insts[x].InstrumentID, pf.Delimiter) if err != nil { return nil, err } @@ -378,7 +382,7 @@ func (ok *Okx) UpdateTickers(ctx context.Context, assetType asset.Item) error { } for y := range ticks { - pair, err := ok.GetPairFromInstrumentID(ticks[y].InstrumentID) + pair, err := currency.NewPairFromString(ticks[y].InstrumentID) if err != nil { return err } @@ -1192,8 +1196,7 @@ allOrders: break allOrders } orderSide := orderList[i].Side - var pair currency.Pair - pair, err = ok.GetPairFromInstrumentID(orderList[i].InstrumentID) + pair, err := currency.NewPairFromString(orderList[i].InstrumentID) if err != nil { return nil, err } @@ -1286,8 +1289,7 @@ allOrders: // reached end of orders to crawl break allOrders } - var pair currency.Pair - pair, err = ok.GetPairFromInstrumentID(orderList[i].InstrumentID) + pair, err := currency.NewPairFromString(orderList[i].InstrumentID) if err != nil { return nil, err } diff --git a/exchanges/sharedtestvalues/sharedtestvalues.go b/exchanges/sharedtestvalues/sharedtestvalues.go index 4962a0e3..5e5a124a 100644 --- a/exchanges/sharedtestvalues/sharedtestvalues.go +++ b/exchanges/sharedtestvalues/sharedtestvalues.go @@ -1,7 +1,6 @@ package sharedtestvalues import ( - "bufio" "bytes" "errors" "fmt" @@ -9,15 +8,14 @@ import ( "path/filepath" "regexp" "strings" - "sync" "testing" "time" - "github.com/stretchr/testify/assert" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/stream" + "github.com/thrasher-corp/gocryptotrader/exchanges/stream/buffer" "github.com/thrasher-corp/gocryptotrader/exchanges/subscription" ) @@ -64,6 +62,7 @@ func NewTestWebsocket() *stream.Websocket { Subscribe: make(chan []subscription.Subscription, 10), Unsubscribe: make(chan []subscription.Subscription, 10), Match: stream.NewMatch(), + Orderbook: buffer.Orderbook{}, } } @@ -154,38 +153,6 @@ func ForceFileStandard(t *testing.T, pattern string) error { return nil } -// TestFixtureToDataHandler takes a new empty exchange and configures a new websocket handler for it, and squirts the json path contents to it -// It accepts a reader function, which is probably e.wsHandleData but could be anything -func TestFixtureToDataHandler(t *testing.T, seed, e exchange.IBotExchange, fixturePath string, reader func([]byte) error) { - t.Helper() - b := e.GetBase() - seedBase := seed.GetBase() - - err := b.CurrencyPairs.Load(&seedBase.CurrencyPairs) - assert.NoError(t, err, "Loading currency pairs should not error") - - b.Name = "fixture" - b.Websocket = &stream.Websocket{ - Wg: new(sync.WaitGroup), - DataHandler: make(chan interface{}, 128), - } - b.API.Endpoints = b.NewEndpoints() - - fixture, err := os.Open(fixturePath) - assert.NoError(t, err, "Opening fixture '%s' should not error", fixturePath) - defer func() { - assert.NoError(t, fixture.Close(), "Closing the fixture file should not error") - }() - - s := bufio.NewScanner(fixture) - for s.Scan() { - msg := s.Bytes() - err := reader(msg) - assert.NoErrorf(t, err, "Fixture message should not error:\n%s", msg) - } - assert.NoError(t, s.Err(), "Fixture Scanner should not error") -} - // SetupCurrencyPairsForExchangeAsset enables an asset for an exchange // and adds the currency pair(s) to the available and enabled list of existing pairs // if it is already enabled or part of the pairs, no error is raised diff --git a/internal/testing/exchange/exchange.go b/internal/testing/exchange/exchange.go index af44c859..997b0d33 100644 --- a/internal/testing/exchange/exchange.go +++ b/internal/testing/exchange/exchange.go @@ -1,12 +1,14 @@ package exchange import ( + "bufio" "context" "errors" "fmt" "log" "net/http" "net/http/httptest" + "os" "path/filepath" "strings" "sync" @@ -16,6 +18,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/thrasher-corp/gocryptotrader/config" + "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" "github.com/thrasher-corp/gocryptotrader/exchanges/mock" "github.com/thrasher-corp/gocryptotrader/exchanges/sharedtestvalues" @@ -152,6 +155,25 @@ func WsMockUpgrader(tb testing.TB, w http.ResponseWriter, r *http.Request, wsHan } } +// FixtureToDataHandler squirts the contents of a file to a reader function (probably e.wsHandleData) +func FixtureToDataHandler(tb testing.TB, fixturePath string, reader func([]byte) error) { + tb.Helper() + + fixture, err := os.Open(fixturePath) + assert.NoError(tb, err, "Opening fixture '%s' should not error", fixturePath) + defer func() { + assert.NoError(tb, fixture.Close(), "Closing the fixture file should not error") + }() + + s := bufio.NewScanner(fixture) + for s.Scan() { + msg := s.Bytes() + err := reader(msg) + assert.NoErrorf(tb, err, "Fixture message should not error:\n%s", msg) + } + assert.NoError(tb, s.Err(), "Fixture Scanner should not error") +} + var setupWsMutex sync.Mutex var setupWsOnce = make(map[exchange.IBotExchange]bool) @@ -182,21 +204,26 @@ func SetupWs(tb testing.TB, e exchange.IBotExchange) { } var updatePairsMutex sync.Mutex -var updatePairsOnce = make(map[exchange.IBotExchange]bool) +var updatePairsOnce = make(map[string]*currency.PairsManager) // UpdatePairsOnce ensures pairs are only updated once in parallel tests +// A clone of the cache of the updated pairs is used to populate duplicate requests func UpdatePairsOnce(tb testing.TB, e exchange.IBotExchange) { tb.Helper() updatePairsMutex.Lock() defer updatePairsMutex.Unlock() - if updatePairsOnce[e] { + b := e.GetBase() + if c, ok := updatePairsOnce[e.GetName()]; ok { + b.CurrencyPairs.Load(c) return } err := e.UpdateTradablePairs(context.Background(), true) require.NoError(tb, err, "UpdateTradablePairs must not error") - updatePairsOnce[e] = true + cache := new(currency.PairsManager) + cache.Load(&b.CurrencyPairs) + updatePairsOnce[e.GetName()] = cache } diff --git a/testdata/configtest.json b/testdata/configtest.json index eb617182..b66ac65e 100644 --- a/testdata/configtest.json +++ b/testdata/configtest.json @@ -667,12 +667,14 @@ }, "useGlobalFormat": true, "lastUpdated": 1566798411, - "assetTypes": [ - "spot", - "futures" - ], "pairs": { + "futures": { + "assetEnabled": true, + "enabled": "", + "available": "" + }, "spot": { + "assetEnabled": true, "enabled": "BTC_JPY,ETH_BTC,BCH_BTC", "available": "BTC_JPY,FXBTC_JPY,ETH_BTC,BCH_BTC" }