Tests: Various race fixes and move TestFixtureToDataHandler (#1534)

* Tests: Move and simplify TestFixtureToDataHandler

* Currency: Fix PairsManager.Load breaking matcher

* Tests: Add multi-instance cache to UpdatePairsOnce

* Kraken: Fix TestUpdateTickers race error

Calling StorePairs on global instance can lead to race

* Bitfinex: Fix TestUpdateTickers racing intermittently

* Currency: Fix concurrent access to PM formats

* Currency: Fix SupportsAsset implementation

This should delegate entirely to PairManager's IsAssetSupported

* Okx: Fix PM intrusion, rm GetPairFromInstrumentID

* Exchange: Fix SetGlobalPairsManager to set asset enabled

* Bitflyer: Fix race on set TestGetCurrURL

TestGetCurrencyTradeURL would fail sometimes due to sequencing of
enabling futures but not having pairs for it.

* Tests: Simplify usage pattern for FixtureToDH
This commit is contained in:
Gareth Kirwan
2024-05-16 06:09:26 +02:00
committed by GitHub
parent 34ef09dad6
commit 7d1eecfa7e
19 changed files with 584 additions and 693 deletions

View File

@@ -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")
}

View File

@@ -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
}

View File

@@ -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")
}

View File

@@ -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

View File

@@ -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)

View File

@@ -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")
}
}

View File

@@ -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) {

View File

@@ -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
}

View File

@@ -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 = &currency.PairFormat{
Uppercase: true,
}
b.CurrencyPairs.RequestFormat = &currency.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, &currency.PairStore{
ConfigFormat: &pFmt,
RequestFormat: &currency.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(&currency.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(&currency.PairFormat{Uppercase: true},
&currency.PairFormat{Uppercase: true})
if err == nil {
t.Error("error cannot be nil")
}
err = b.SetGlobalPairsManager(&currency.PairFormat{Uppercase: true}, &currency.PairFormat{Uppercase: true})
assert.ErrorContains(t, err, " cannot set pairs manager, no assets provided")
err = b.SetGlobalPairsManager(&currency.PairFormat{Uppercase: true},
&currency.PairFormat{Uppercase: true}, asset.Empty)
if err == nil {
t.Error("error cannot be nil")
}
err = b.SetGlobalPairsManager(&currency.PairFormat{Uppercase: true}, &currency.PairFormat{Uppercase: true}, asset.Empty)
assert.ErrorContains(t, err, " cannot set global pairs manager config pair format requires delimiter for assets")
err = b.SetGlobalPairsManager(&currency.PairFormat{Uppercase: true},
&currency.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(&currency.PairFormat{Uppercase: true},
&currency.PairFormat{Uppercase: true, Delimiter: currency.DashDelimiter},
asset.Spot,
asset.Binary)
if err != nil {
t.Error(err)
}
err = b.SetGlobalPairsManager(&currency.PairFormat{Uppercase: true}, &currency.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(&currency.PairFormat{Uppercase: true},
&currency.PairFormat{Uppercase: true}, asset.Spot, asset.Binary)
if err == nil {
t.Error("error cannot be nil")
}
err = b.SetGlobalPairsManager(&currency.PairFormat{Uppercase: true}, &currency.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, &currency.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, &currency.PairStore{
AssetEnabled: convert.BoolPtr(true),

View File

@@ -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

View File

@@ -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) {

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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"}]}`

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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"
}