Bitfinex: Fix panic on TestUpdateTickers (#1454)

* Bitfinex: Handle Errors in tickers without panic

* Bitfinex: Use TypeAssertError for tickers

* Bitfinex: Refactor and improve ticker handling

* Unify Ticker response handling
* Simplify/Humanify errors in parsing ticker responses
* Remove polymorphic response handling for < 10 fields, seems [antiquated according to docs](https://docs.bitfinex.com/reference/rest-public-ticker)
* Add test coverage for tickers

* Bitfinex: Ignore resp format errs in GetTickers

We're still getting:
`received 'invalid ticker response format for tALT2612:USD field BidSize
from [100 <nil> 100 <nil> <nil> <nil> <nil> <nil> <nil> <nil>]`
too frequently right now.

Considered:
* Special casing tALT2612:*; However if it's happening with this curr
  it'll probably happen again
* Warning about the error; However it'd just be persistent,
  unactionable and annoying  noise in the logs of a running server

So the conclusion is to just silently ignore it

* Bitfinex: Accept locked market in TestUpdateTicker
This commit is contained in:
Gareth Kirwan
2024-01-25 06:56:08 +01:00
committed by GitHub
parent 804cee4287
commit 23c82bead4
3 changed files with 192 additions and 197 deletions

View File

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

View File

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

View File

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