diff --git a/exchanges/bitfinex/bitfinex.go b/exchanges/bitfinex/bitfinex.go index f4a52a95..e76a23ad 100644 --- a/exchanges/bitfinex/bitfinex.go +++ b/exchanges/bitfinex/bitfinex.go @@ -625,8 +625,8 @@ func (b *Bitfinex) GetDerivativeStatusInfo(ctx context.Context, keys, startTime, } // GetTickerBatch returns all supported ticker information -func (b *Bitfinex) GetTickerBatch(ctx context.Context) (map[string]Ticker, error) { - var response [][]interface{} +func (b *Bitfinex) GetTickerBatch(ctx context.Context) (map[string]*Ticker, error) { + var response [][]any path := bitfinexAPIVersion2 + bitfinexTickerBatch + "?symbols=ALL" @@ -636,106 +636,29 @@ func (b *Bitfinex) GetTickerBatch(ctx context.Context) (map[string]Ticker, error return nil, err } - var tickers = make(map[string]Ticker) - for x := range response { - symbol, ok := response[x][0].(string) + var tickErrs error + var tickers = make(map[string]*Ticker) + for _, tickResp := range response { + symbol, ok := tickResp[0].(string) if !ok { - return nil, common.GetTypeAssertError("string", response[x][0], "Ticker.Symbol") - } - - var t Ticker - if len(response[x]) > 11 { - if t.FlashReturnRate, ok = response[x][1].(float64); !ok { - return nil, common.GetTypeAssertError("float64", response[x][1], "Ticker.Data.FlashReturnRate") - } - if t.Bid, ok = response[x][2].(float64); !ok { - return nil, common.GetTypeAssertError("float64", response[x][2], "Ticker.Data.Bid") - } - var bidPeriod float64 - bidPeriod, ok = response[x][3].(float64) - if !ok { - return nil, common.GetTypeAssertError("float64", response[x][3], "Ticker.Data.BidPeriod") - } - t.BidPeriod = int64(bidPeriod) - if t.BidSize, ok = response[x][4].(float64); !ok { - return nil, common.GetTypeAssertError("float64", response[x][4], "Ticker.Data.BidSize") - } - if t.Ask, ok = response[x][5].(float64); !ok { - return nil, common.GetTypeAssertError("float64", response[x][5], "Ticker.Data.Ask") - } - var askPeriod float64 - askPeriod, ok = response[x][6].(float64) - if !ok { - return nil, common.GetTypeAssertError("float64", response[x][6], "Ticker.Data.AskPeriod") - } - t.AskPeriod = int64(askPeriod) - if t.AskSize, ok = response[x][7].(float64); !ok { - return nil, common.GetTypeAssertError("float64", response[x][7], "Ticker.Data.AskSize") - } - if t.DailyChange, ok = response[x][8].(float64); !ok { - return nil, common.GetTypeAssertError("float64", response[x][8], "Ticker.Data.DailyChange") - } - if t.DailyChangePerc, ok = response[x][9].(float64); !ok { - return nil, common.GetTypeAssertError("float64", response[x][9], "Ticker.Data.DailyChangePercentage") - } - if t.Last, ok = response[x][10].(float64); !ok { - return nil, common.GetTypeAssertError("float64", response[x][10], "Ticker.Data.LastPrice") - } - if t.Volume, ok = response[x][11].(float64); !ok { - return nil, common.GetTypeAssertError("float64", response[x][11], "Ticker.Data.DailyVolume") - } - if t.High, ok = response[x][12].(float64); !ok { - return nil, common.GetTypeAssertError("float64", response[x][12], "Ticker.Data.DailyHigh") - } - if t.Low, ok = response[x][13].(float64); !ok { - return nil, common.GetTypeAssertError("float64", response[x][13], "Ticker.Data.DailyLow") - } - if t.FFRAmountAvailable, ok = response[x][16].(float64); !ok { - return nil, common.GetTypeAssertError("float64", response[x][16], "Ticker.Data.FFRAmountAvailable") - } - - tickers[symbol] = t + tickErrs = common.AppendError(tickErrs, fmt.Errorf("%w: %v", errTickerInvalidSymbol, symbol)) continue } - - if t.Bid, ok = response[x][1].(float64); !ok { - return nil, common.GetTypeAssertError("float64", response[x][1], "Tickers.Symbol.Bid") + if t, err := tickerFromResp(symbol, tickResp[1:]); err != nil { + // We get too frequent intermittent formatting errors from tALT2612:USD to treat them as errors + if !errors.Is(err, errTickerInvalidResp) { + tickErrs = common.AppendError(tickErrs, err) + } + } else { + tickers[symbol] = t } - if t.BidSize, ok = response[x][2].(float64); !ok { - return nil, common.GetTypeAssertError("float64", response[x][2], "Tickers.Symbol.BidSize") - } - if t.Ask, ok = response[x][3].(float64); !ok { - return nil, common.GetTypeAssertError("float64", response[x][3], "Tickers.Symbol.Ask") - } - if t.AskSize, ok = response[x][4].(float64); !ok { - return nil, common.GetTypeAssertError("float64", response[x][4], "Tickers.Symbol.AskSize") - } - if t.DailyChange, ok = response[x][5].(float64); !ok { - return nil, common.GetTypeAssertError("float64", response[x][5], "Tickers.Symbol.DailyChange") - } - if t.DailyChangePerc, ok = response[x][6].(float64); !ok { - return nil, common.GetTypeAssertError("float64", response[x][6], "Tickers.Symbol.DailyChangeRelative") - } - if t.Last, ok = response[x][7].(float64); !ok { - return nil, common.GetTypeAssertError("float64", response[x][7], "Tickers.Symbol.LastPrice") - } - if t.Volume, ok = response[x][8].(float64); !ok { - return nil, common.GetTypeAssertError("float64", response[x][8], "Tickers.Symbol.DailyVolume") - } - if t.High, ok = response[x][9].(float64); !ok { - return nil, common.GetTypeAssertError("float64", response[x][9], "Tickers.Symbol.DailyHigh") - } - if t.Low, ok = response[x][10].(float64); !ok { - return nil, common.GetTypeAssertError("float64", response[x][10], "Tickers.Symbol.DailyLow") - } - tickers[symbol] = t } - return tickers, nil + return tickers, tickErrs } // GetTicker returns ticker information for one symbol func (b *Bitfinex) GetTicker(ctx context.Context, symbol string) (*Ticker, error) { - var response []interface{} + var response []any path := bitfinexAPIVersion2 + bitfinexTicker + symbol @@ -744,92 +667,77 @@ func (b *Bitfinex) GetTicker(ctx context.Context, symbol string) (*Ticker, error return nil, err } - var t Ticker - if len(response) > 10 { - var ok bool - if t.FlashReturnRate, ok = response[0].(float64); !ok { - return nil, errors.New("unable to type assert flashReturnRate") - } - if t.Bid, ok = response[1].(float64); !ok { - return nil, errors.New("unable to type assert bid") - } - var bidPeriod float64 - bidPeriod, ok = response[2].(float64) - if !ok { - return nil, errors.New("unable to type assert bidPeriod") - } - t.BidPeriod = int64(bidPeriod) - if t.BidSize, ok = response[3].(float64); !ok { - return nil, errors.New("unable to type assert bidSize") - } - if t.Ask, ok = response[4].(float64); !ok { - return nil, errors.New("unable to type assert ask") - } - var askPeriod float64 - askPeriod, ok = response[5].(float64) - if !ok { - return nil, errors.New("unable to type assert askPeriod") - } - t.AskPeriod = int64(askPeriod) - if t.AskSize, ok = response[6].(float64); !ok { - return nil, errors.New("unable to type assert askSize") - } - if t.DailyChange, ok = response[7].(float64); !ok { - return nil, errors.New("unable to type assert dailyChange") - } - if t.DailyChangePerc, ok = response[8].(float64); !ok { - return nil, errors.New("unable to type assert dailyChangePerc") - } - if t.Last, ok = response[9].(float64); !ok { - return nil, errors.New("unable to type assert last") - } - if t.Volume, ok = response[10].(float64); !ok { - return nil, errors.New("unable to type assert volume") - } - if t.High, ok = response[11].(float64); !ok { - return nil, errors.New("unable to type assert high") - } - if t.Low, ok = response[12].(float64); !ok { - return nil, errors.New("unable to type assert low") - } - if t.FFRAmountAvailable, ok = response[15].(float64); !ok { - return nil, errors.New("unable to type assert FFRAmountAvailable") - } - return &t, nil + t, err := tickerFromResp(symbol, response) + if err != nil { + return nil, err } + return t, nil +} - var ok bool - if t.Bid, ok = response[0].(float64); !ok { - return nil, errors.New("unable to type assert bid") +var tickerFields = []string{"Bid", "BidSize", "Ask", "AskSize", "DailyChange", "DailyChangePercentage", "LastPrice", "DailyVolume", "DailyHigh", "DailyLow"} + +func tickerFromResp(symbol string, respAny []any) (*Ticker, error) { + if strings.HasPrefix(symbol, "f") { + return tickerFromFundingResp(symbol, respAny) } - if t.BidSize, ok = response[1].(float64); !ok { - return nil, errors.New("unable to type assert bidSize") + if len(respAny) != 10 { + return nil, fmt.Errorf("%w for %s: %v", errTickerInvalidFieldCount, symbol, respAny) } - if t.Ask, ok = response[2].(float64); !ok { - return nil, errors.New("unable to type assert ask") + resp := make([]float64, 10) + for i := range respAny { + f, ok := respAny[i].(float64) + if !ok { + return nil, fmt.Errorf("%w for %s field %s from %v", errTickerInvalidResp, symbol, tickerFields[i], respAny) + } + resp[i] = f } - if t.AskSize, ok = response[3].(float64); !ok { - return nil, errors.New("unable to type assert askSize") + return &Ticker{ + Bid: resp[0], + BidSize: resp[1], + Ask: resp[2], + AskSize: resp[3], + DailyChange: resp[4], + DailyChangePerc: resp[5], + Last: resp[6], + Volume: resp[7], + High: resp[8], + Low: resp[9], + }, nil +} + +var fundingTickerFields = []string{"FlashReturnRate", "Bid", "BidPeriod", "BidSize", "Ask", "AskPeriod", "AskSize", "DailyChange", "DailyChangePercentage", "LastPrice", "DailyVolume", "DailyHigh", "DailyLow", "", "", "FFRAmountAvailable"} + +func tickerFromFundingResp(symbol string, respAny []any) (*Ticker, error) { + if len(respAny) != 16 { + return nil, fmt.Errorf("%w for %s: %v", errTickerInvalidFieldCount, symbol, respAny) } - if t.DailyChange, ok = response[4].(float64); !ok { - return nil, errors.New("unable to type assert dailyChange") + resp := make([]float64, 16) + for i := range respAny { + if fundingTickerFields[i] == "" { // Unused nil fields + continue + } + f, ok := respAny[i].(float64) + if !ok { + return nil, fmt.Errorf("%w for %s field %s from %v", errTickerInvalidResp, symbol, fundingTickerFields[i], respAny) + } + resp[i] = f } - if t.DailyChangePerc, ok = response[5].(float64); !ok { - return nil, errors.New("unable to type assert dailyChangePerc") - } - if t.Last, ok = response[6].(float64); !ok { - return nil, errors.New("unable to type assert last") - } - if t.Volume, ok = response[7].(float64); !ok { - return nil, errors.New("unable to type assert volume") - } - if t.High, ok = response[8].(float64); !ok { - return nil, errors.New("unable to type assert high") - } - if t.Low, ok = response[9].(float64); !ok { - return nil, errors.New("unable to type assert low") - } - return &t, nil + return &Ticker{ + FlashReturnRate: resp[0], + Bid: resp[1], + BidPeriod: int64(resp[2]), + BidSize: resp[3], + Ask: resp[4], + AskPeriod: int64(resp[5]), + AskSize: resp[6], + DailyChange: resp[7], + DailyChangePerc: resp[8], + Last: resp[9], + Volume: resp[10], + High: resp[11], + Low: resp[12], + FFRAmountAvailable: resp[15], + }, nil } // GetTrades gets historic trades that occurred on the exchange diff --git a/exchanges/bitfinex/bitfinex_test.go b/exchanges/bitfinex/bitfinex_test.go index d594bb06..14935176 100644 --- a/exchanges/bitfinex/bitfinex_test.go +++ b/exchanges/bitfinex/bitfinex_test.go @@ -13,6 +13,7 @@ import ( "github.com/buger/jsonparser" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/core" @@ -241,23 +242,108 @@ func TestGetPlatformStatus(t *testing.T) { func TestGetTickerBatch(t *testing.T) { t.Parallel() - _, err := b.GetTickerBatch(context.Background()) - if err != nil { - t.Error(err) - } + ticks, err := b.GetTickerBatch(context.Background()) + require.NoError(t, err, "GetTickerBatch should not error") + require.NotEmpty(t, ticks, "GetTickerBatch should return some ticks") + require.Contains(t, ticks, "tBTCUSD", "Ticker batch must contain tBTCUSD") + checkTradeTick(t, ticks["tBTCUSD"]) + require.Contains(t, ticks, "fUSD", "Ticker batch must contain fUSD") + checkTradeTick(t, ticks["fUSD"]) } func TestGetTicker(t *testing.T) { t.Parallel() - _, err := b.GetTicker(context.Background(), "tBTCUSD") - if err != nil { - t.Error(err) - } + tick, err := b.GetTicker(context.Background(), "tBTCUSD") + require.NoError(t, err, "GetTicker should not error") + checkTradeTick(t, tick) +} - _, err = b.GetTicker(context.Background(), "fUSD") - if err != nil { - t.Error(err) - } +func TestTickerFromResp(t *testing.T) { + t.Parallel() + _, err := tickerFromResp("tBTCUSD", []any{100.0, nil, 100.0, nil, nil, nil, nil, nil, nil, nil}) + assert.ErrorIs(t, err, errTickerInvalidResp, "tickerFromResp should error correctly") + assert.ErrorContains(t, err, "BidSize", "tickerFromResp should error correctly") + assert.ErrorContains(t, err, "tBTCUSD", "tickerFromResp should error correctly") + + _, err = tickerFromResp("tBTCUSD", []any{100.0, nil, 100.0, nil, nil, nil, nil, nil, nil}) + assert.ErrorIs(t, err, errTickerInvalidFieldCount, "tickerFromResp should error correctly") + assert.ErrorContains(t, err, "tBTCUSD", "tickerFromResp should error correctly") + + tick, err := tickerFromResp("tBTCUSD", []any{1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8, 9.9, 10.10}) + require.NoError(t, err, "tickerFromResp should error correctly") + assert.Equal(t, 1.1, tick.Bid, "Tick Bid should be correct") + assert.Equal(t, 2.2, tick.BidSize, "Tick BidSize should be correct") + assert.Equal(t, 3.3, tick.Ask, "Tick Ask should be correct") + assert.Equal(t, 4.4, tick.AskSize, "Tick AskSize should be correct") + assert.Equal(t, 5.5, tick.DailyChange, "Tick DailyChange should be correct") + assert.Equal(t, 6.6, tick.DailyChangePerc, "Tick DailyChangePerc should be correct") + assert.Equal(t, 7.7, tick.Last, "Tick Last should be correct") + assert.Equal(t, 8.8, tick.Volume, "Tick Volume should be correct") + assert.Equal(t, 9.9, tick.High, "Tick High should be correct") + assert.Equal(t, 10.10, tick.Low, "Tick Low should be correct") + + _, err = tickerFromResp("fBTC", []any{100.0, nil, 100.0, nil, nil, nil, nil, nil, nil, nil}) + assert.ErrorIs(t, err, errTickerInvalidFieldCount, "tickerFromResp should delegate to tickerFromFundingResp and error correctly") + assert.ErrorContains(t, err, "fBTC", "tickerFromResp should delegate to tickerFromFundingResp and error correctly") +} + +func TestTickerFromFundingResp(t *testing.T) { + t.Parallel() + _, err := tickerFromFundingResp("fBTC", []any{nil, 100.0, nil, 100.0, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil}) + assert.ErrorIs(t, err, errTickerInvalidResp, "tickerFromFundingResp should error correctly") + assert.ErrorContains(t, err, "FlashReturnRate", "tickerFromFundingResp should error correctly") + assert.ErrorContains(t, err, "fBTC", "tickerFromFundingResp should error correctly") + + _, err = tickerFromFundingResp("fBTC", []any{100.0, nil, 100.0, nil, nil, nil, nil, nil, nil}) + assert.ErrorIs(t, err, errTickerInvalidFieldCount, "tickerFromFundingResp should error correctly") + assert.ErrorContains(t, err, "fBTC", "tickerFromFundingResp should error correctly") + + tick, err := tickerFromFundingResp("fBTC", []any{1.1, 2.2, 3.0, 4.4, 5.5, 6.0, 7.7, 8.8, 9.9, 10.10, 11.11, 12.12, 13.13, nil, nil, 15.15}) + require.NoError(t, err, "tickerFromFundingResp should error correctly") + assert.Equal(t, 1.1, tick.FlashReturnRate, "Tick FlashReturnRate should be correct") + assert.Equal(t, 2.2, tick.Bid, "Tick Bid should be correct") + assert.Equal(t, int64(3), tick.BidPeriod, "Tick BidPeriod should be correct") + assert.Equal(t, 4.4, tick.BidSize, "Tick BidSize should be correct") + assert.Equal(t, 5.5, tick.Ask, "Tick Ask should be correct") + assert.Equal(t, int64(6), tick.AskPeriod, "Tick AskPeriod should be correct") + assert.Equal(t, 7.7, tick.AskSize, "Tick AskSize should be correct") + assert.Equal(t, 8.8, tick.DailyChange, "Tick DailyChange should be correct") + assert.Equal(t, 9.9, tick.DailyChangePerc, "Tick DailyChangePerc should be correct") + assert.Equal(t, 10.10, tick.Last, "Tick Last should be correct") + assert.Equal(t, 11.11, tick.Volume, "Tick Volume should be correct") + assert.Equal(t, 12.12, tick.High, "Tick High should be correct") + assert.Equal(t, 13.13, tick.Low, "Tick Low should be correct") + assert.Equal(t, 15.15, tick.FFRAmountAvailable, "Tick FFRAmountAvailable should be correct") +} + +func TestGetTickerFunding(t *testing.T) { + t.Parallel() + tick, err := b.GetTicker(context.Background(), "fUSD") + require.NoError(t, err, "GetTicker should not error") + checkFundingTick(t, tick) +} + +func checkTradeTick(tb testing.TB, tick *Ticker) { + tb.Helper() + assert.Positive(tb, tick.Bid, "Tick Bid should be positive") + assert.Positive(tb, tick.BidSize, "Tick BidSize should be positive") + assert.Positive(tb, tick.Ask, "Tick Ask should be positive") + assert.Positive(tb, tick.AskSize, "Tick AskSize should be positive") + assert.Positive(tb, tick.Last, "Tick Last should be positive") + // Can't test DailyChange*, Volume, High or Low without false positives when they're occasionally 0 +} + +func checkFundingTick(tb testing.TB, tick *Ticker) { + tb.Helper() + assert.NotZero(tb, tick.FlashReturnRate, "Tick FlashReturnRate should not be zero") + assert.Positive(tb, tick.Bid, "Tick Bid should be positive") + assert.Positive(tb, tick.BidPeriod, "Tick BidPeriod should be positive") + assert.Positive(tb, tick.BidSize, "Tick BidSize should be positive") + assert.Positive(tb, tick.Ask, "Tick Ask should be positive") + assert.Positive(tb, tick.AskPeriod, "Tick AskPeriod should be positive") + assert.Positive(tb, tick.AskSize, "Tick AskSize should be positive") + assert.Positive(tb, tick.Last, "Tick Last should be positive") + assert.Positive(tb, tick.FFRAmountAvailable, "Tick FFRAmountavailable should be positive") } func TestGetTrades(t *testing.T) { @@ -478,9 +564,8 @@ func TestNewOrder(t *testing.T) { func TestUpdateTicker(t *testing.T) { t.Parallel() - if _, err := b.UpdateTicker(context.Background(), btcusdPair, asset.Spot); err != nil { - t.Error(err) - } + _, err := b.UpdateTicker(context.Background(), btcusdPair, asset.Spot) + assert.NoError(t, common.ExcludeError(err, ticker.ErrBidEqualsAsk), "UpdateTicker may only error about locked markets") } func TestUpdateTickers(t *testing.T) { @@ -491,13 +576,13 @@ func TestUpdateTickers(t *testing.T) { assets := b.GetAssetTypes(false) for _, a := range assets { avail, err := b.GetAvailablePairs(a) - assert.NoError(t, err, "GetAvailablePairs should not error") + require.NoError(t, err, "GetAvailablePairs should not error") err = b.CurrencyPairs.StorePairs(a, avail, true) - assert.NoError(t, err, "StorePairs should not error") + require.NoError(t, err, "StorePairs should not error") err = b.UpdateTickers(context.Background(), a) - assert.NoError(t, common.ExcludeError(err, ticker.ErrBidEqualsAsk), "UpdateTickers may only error about locked markets") + require.NoError(t, common.ExcludeError(err, ticker.ErrBidEqualsAsk), "UpdateTickers may only error about locked markets") // Bitfinex leaves delisted pairs in Available info/conf endpoints // We want to assert that most pairs are valid, so we'll check that no more than 5% are erroring @@ -512,7 +597,7 @@ func TestUpdateTickers(t *testing.T) { } } if !assert.Greater(t, okay/float64(len(avail))*100.0, acceptableThreshold, "At least %.f%% of %s tickers should not error", acceptableThreshold, a) { - t.Log(errs.Error()) + assert.NoError(t, errs, "Collection of all the ticker errors") } } } diff --git a/exchanges/bitfinex/bitfinex_types.go b/exchanges/bitfinex/bitfinex_types.go index e99a1171..5fea5f65 100644 --- a/exchanges/bitfinex/bitfinex_types.go +++ b/exchanges/bitfinex/bitfinex_types.go @@ -11,12 +11,14 @@ import ( ) var ( - errSetCannotBeEmpty = errors.New("set cannot be empty") - errTypeAssert = errors.New("type assertion failed") - errNoSeqNo = errors.New("no sequence number") - errUnknownError = errors.New("unknown error") - errParamNotAllowed = errors.New("param not allowed") - errParsingWSField = errors.New("error parsing WS field") + errSetCannotBeEmpty = errors.New("set cannot be empty") + errNoSeqNo = errors.New("no sequence number") + errUnknownError = errors.New("unknown error") + errParamNotAllowed = errors.New("param not allowed") + errParsingWSField = errors.New("error parsing WS field") + errTickerInvalidSymbol = errors.New("invalid ticker symbol") + errTickerInvalidResp = errors.New("invalid ticker response format") + errTickerInvalidFieldCount = errors.New("invalid ticker response field count") ) // AccountV2Data stores account v2 data