Huobi: Fix intermittent UpdateTickers failure on expiry (#1655)

* Huobi: Return all errors from UpdateTickers

* Huobi: Add contract type cache for expiry codes
This commit is contained in:
Gareth Kirwan
2024-10-01 06:00:20 +01:00
committed by GitHub
parent d31fa3ff3d
commit 3dd7838a60
5 changed files with 195 additions and 248 deletions

View File

@@ -10,6 +10,7 @@ import (
"net/url"
"strconv"
"strings"
"sync"
"time"
"github.com/thrasher-corp/gocryptotrader/common"
@@ -71,14 +72,16 @@ const (
huobiCurrenciesReference = "/v2/reference/currencies"
huobiWithdrawHistory = "/query/deposit-withdraw"
huobiBatchCoinMarginSwapContracts = "/v2/swap-ex/market/detail/batch_merged"
huobiBatchLinearSwapContracts = "/linear-swap-ex/market/detail/batch_merged"
huobiBatchLinearSwapContracts = "/v2/linear-swap-ex/market/detail/batch_merged"
huobiBatchContracts = "/v2/market/detail/batch_merged"
)
// HUOBI is the overarching type across this package
type HUOBI struct {
exchange.Base
AccountID string
AccountID string
futureContractCodesMutex sync.RWMutex
futureContractCodes map[string]currency.Code
}
// GetMarginRates gets margin rates

View File

@@ -9,7 +9,9 @@ import (
"io"
"net/http"
"net/url"
"slices"
"strconv"
"strings"
"time"
"github.com/thrasher-corp/gocryptotrader/common"
@@ -77,11 +79,12 @@ const (
fTriggerOrderHistory = "/api/v1/contract_trigger_hisorders"
uContractOpenInterest = "/linear-swap-api/v1/swap_open_interest"
fContractDateFormat = "060102"
)
var errInvalidContractType = errors.New("invalid contract type")
var (
errInvalidContractType = errors.New("invalid contract type")
errInconsistentContractExpiry = errors.New("inconsistent contract expiry date codes")
)
// FGetContractInfo gets contract info for futures
func (h *HUOBI) FGetContractInfo(ctx context.Context, symbol, contractType string, code currency.Pair) (FContractInfoData, error) {
@@ -90,11 +93,11 @@ func (h *HUOBI) FGetContractInfo(ctx context.Context, symbol, contractType strin
if symbol != "" {
params.Set("symbol", symbol)
}
if contractType != "" {
if !common.StringSliceCompareInsensitive(validContractTypes, contractType) {
return resp, errors.New("invalid contractType")
if t := strings.ToLower(contractType); t != "" {
if _, ok := contractExpiryNames[t]; !ok {
return resp, fmt.Errorf("%w: %v", errInvalidContractType, t)
}
params.Set("contract_type", contractType)
params.Set("contract_type", t)
}
if !code.IsEmpty() {
codeValue, err := h.FormatSymbol(code, asset.Futures)
@@ -129,11 +132,11 @@ func (h *HUOBI) FContractPriceLimitations(ctx context.Context, symbol, contractT
if symbol != "" {
params.Set("symbol", symbol)
}
if contractType != "" {
if !common.StringSliceCompareInsensitive(validContractTypes, contractType) {
return resp, fmt.Errorf("invalid contractType: %s", contractType)
if t := strings.ToLower(contractType); t != "" {
if _, ok := contractExpiryNames[t]; !ok {
return resp, fmt.Errorf("%w: %v", errInvalidContractType, t)
}
params.Set("contract_type", contractType)
params.Set("contract_type", t)
}
if !code.IsEmpty() {
codeValue, err := h.FormatSymbol(code, asset.Futures)
@@ -163,11 +166,11 @@ func (h *HUOBI) ContractOpenInterestUSDT(ctx context.Context, contractCode, pair
}
params.Set("pair", p)
}
if contractType != "" {
if !common.StringSliceCompareInsensitive(validContractTypes, contractType) {
return nil, errors.New("invalid contractType")
if t := strings.ToLower(contractType); t != "" {
if _, ok := contractExpiryNames[t]; !ok {
return nil, fmt.Errorf("%w: %v", errInvalidContractType, t)
}
params.Set("contract_type", contractType)
params.Set("contract_type", t)
}
if businessType != "" {
params.Set("business_type", businessType)
@@ -186,11 +189,11 @@ func (h *HUOBI) FContractOpenInterest(ctx context.Context, symbol, contractType
if symbol != "" {
params.Set("symbol", symbol)
}
if contractType != "" {
if !common.StringSliceCompareInsensitive(validContractTypes, contractType) {
return resp, errors.New("invalid contractType")
if t := strings.ToLower(contractType); t != "" {
if _, ok := contractExpiryNames[t]; !ok {
return resp, fmt.Errorf("%w: %v", errInvalidContractType, t)
}
params.Set("contract_type", contractType)
params.Set("contract_type", t)
}
if !code.IsEmpty() {
codeValue, err := h.formatFuturesPair(code, true)
@@ -375,8 +378,9 @@ func (h *HUOBI) FQueryHisOpenInterest(ctx context.Context, symbol, contractType,
if symbol != "" {
params.Set("symbol", symbol)
}
if !common.StringSliceCompareInsensitive(validContractTypes, contractType) {
return resp, fmt.Errorf("%w %v", errInvalidContractType, contractType)
contractType = strings.ToLower(contractType)
if _, ok := contractExpiryNames[contractType]; !ok {
return resp, fmt.Errorf("%w: %v", errInvalidContractType, contractType)
}
params.Set("contract_type", contractType)
if !common.StringSliceCompareInsensitive(validPeriods, period) {
@@ -749,11 +753,11 @@ func (h *HUOBI) FOrder(ctx context.Context, contractCode currency.Pair, symbol,
if symbol != "" {
req["symbol"] = symbol
}
if contractType != "" {
if !common.StringSliceCompareInsensitive(validContractTypes, contractType) {
return resp, errors.New("invalid contractType")
if t := strings.ToLower(contractType); t != "" {
if _, ok := contractExpiryNames[t]; !ok {
return resp, fmt.Errorf("%w: %v", errInvalidContractType, t)
}
req["contract_type"] = contractType
req["contract_type"] = t
}
if !contractCode.IsEmpty() {
codeValue, err := h.FormatSymbol(contractCode, asset.Futures)
@@ -807,8 +811,8 @@ func (h *HUOBI) FPlaceBatchOrder(ctx context.Context, data []fBatchOrderData) (F
data[x].ContractCode = formattedPair.String()
}
if data[x].ContractType != "" {
if !common.StringSliceCompareInsensitive(validContractTypes, data[x].ContractType) {
return resp, errors.New("invalid contractType")
if _, ok := contractExpiryNames[strings.ToLower(data[x].ContractType)]; !ok {
return resp, fmt.Errorf("%w %v", errInvalidContractType, data[x].ContractType)
}
}
if !common.StringSliceCompareInsensitive(validOffsetTypes, data[x].Offset) {
@@ -846,11 +850,11 @@ func (h *HUOBI) FCancelAllOrders(ctx context.Context, contractCode currency.Pair
if symbol != "" {
req["symbol"] = symbol
}
if contractType != "" {
if !common.StringSliceCompareInsensitive(validContractTypes, contractType) {
return resp, errors.New("invalid contractType")
if t := strings.ToLower(contractType); t != "" {
if _, ok := contractExpiryNames[t]; !ok {
return resp, fmt.Errorf("%w: %v", errInvalidContractType, t)
}
req["contract_type"] = contractType
req["contract_type"] = t
}
if !contractCode.IsEmpty() {
codeValue, err := h.FormatSymbol(contractCode, asset.Futures)
@@ -867,11 +871,11 @@ func (h *HUOBI) FFlashCloseOrder(ctx context.Context, contractCode currency.Pair
var resp FOrderData
req := make(map[string]interface{})
req["symbol"] = symbol
if contractType != "" {
if !common.StringSliceCompareInsensitive(validContractTypes, contractType) {
return resp, errors.New("invalid contractType")
if t := strings.ToLower(contractType); t != "" {
if _, ok := contractExpiryNames[t]; !ok {
return resp, fmt.Errorf("%w: %v", errInvalidContractType, t)
}
req["contract_type"] = contractType
req["contract_type"] = t
}
if !contractCode.IsEmpty() {
codeValue, err := h.FormatSymbol(contractCode, asset.Futures)
@@ -1039,11 +1043,11 @@ func (h *HUOBI) FPlaceTriggerOrder(ctx context.Context, contractCode currency.Pa
if symbol != "" {
req["symbol"] = symbol
}
if contractType != "" {
if !common.StringSliceCompareInsensitive(validContractTypes, contractType) {
return resp, fmt.Errorf("invalid contractType: %s", contractType)
if t := strings.ToLower(contractType); t != "" {
if _, ok := contractExpiryNames[t]; !ok {
return resp, fmt.Errorf("%w: %v", errInvalidContractType, t)
}
req["contract_type"] = contractType
req["contract_type"] = t
}
if !contractCode.IsEmpty() {
codeValue, err := h.FormatSymbol(contractCode, asset.Futures)
@@ -1094,11 +1098,11 @@ func (h *HUOBI) FCancelAllTriggerOrders(ctx context.Context, contractCode curren
}
req["contract_code"] = codeValue
}
if contractType != "" {
if !common.StringSliceCompareInsensitive(validContractTypes, contractType) {
return resp, nil
if t := strings.ToLower(contractType); t != "" {
if _, ok := contractExpiryNames[t]; !ok {
return resp, fmt.Errorf("%w: %v", errInvalidContractType, t)
}
req["contract_type"] = contractType
req["contract_type"] = t
}
return resp, h.FuturesAuthenticatedHTTPRequest(ctx, exchange.RestFutures, http.MethodPost, fCancelAllTriggerOrders, nil, req, &resp)
}
@@ -1256,9 +1260,9 @@ func (h *HUOBI) formatFuturesCode(p currency.Code) (string, error) {
// formatFuturesPair handles pairs in the format as "BTC-NW" and "BTC210827"
func (h *HUOBI) formatFuturesPair(p currency.Pair, convertQuoteToExpiry bool) (string, error) {
if common.StringSliceCompareInsensitive(validContractShortTypes, p.Quote.String()) {
if slices.Contains(validContractExpiryCodes, strings.ToUpper(p.Quote.String())) {
if convertQuoteToExpiry {
cp, err := h.convertContractShortHandToExpiry(p, time.Now())
cp, err := h.pairFromContractExpiryCode(p)
if err != nil {
return "", err
}
@@ -1273,42 +1277,16 @@ func (h *HUOBI) formatFuturesPair(p currency.Pair, convertQuoteToExpiry bool) (s
return h.FormatSymbol(p, asset.Futures)
}
// convertContractShortHandToExpiry converts a contract shorthand eg BTC-CW into a full expiry date
// eg BTC240329 to associate with tradable pair formatting
func (h *HUOBI) convertContractShortHandToExpiry(pair currency.Pair, tt time.Time) (currency.Pair, error) {
loc, err := time.LoadLocation("Asia/Singapore")
if err != nil {
return currency.EMPTYPAIR, err
// pairFromContractExpiryCode converts a pair with contract expiry shorthand in the Quote to a concrete tradable pair
// We need this because some apis, such as ticker, use BTC_CW, NW, CQ, NQ
// Other apis, such as contract_info, use contract type of this_week, next_week, quarter (sic), and next_quater
func (h *HUOBI) pairFromContractExpiryCode(p currency.Pair) (currency.Pair, error) {
h.futureContractCodesMutex.RLock()
defer h.futureContractCodesMutex.RUnlock()
exp, ok := h.futureContractCodes[p.Quote.String()]
if !ok {
return p, fmt.Errorf("%w: %s", errInvalidContractType, p.Quote.String())
}
tt = tt.In(loc)
switch pair.Quote.Item.Symbol {
case "NW":
tt = tt.AddDate(0, 0, 7)
fallthrough
case "CW":
for {
if tt.Weekday() == time.Friday {
break
}
tt = tt.AddDate(0, 0, 1)
}
case "NQ":
tt = tt.AddDate(0, 3, 0)
fallthrough
case "CQ":
// Find the next quarter end
for !(tt.Month() == time.March || tt.Month() == time.June || tt.Month() == time.September || tt.Month() == time.December) {
tt = tt.AddDate(0, 1, 0)
}
// Find the last day of the quarter
tt = time.Date(tt.Year(), tt.Month()+1, 0, 0, 0, 0, 0, time.UTC)
// Find the last Friday of the quarter
for tt.Weekday() != time.Friday {
tt = tt.AddDate(0, 0, -1)
}
default:
return currency.EMPTYPAIR, fmt.Errorf(" %w %v", errInvalidContractType, pair)
}
pair.Quote = currency.NewCode(tt.Format(fContractDateFormat))
return pair, nil
p.Quote = exp
return p, nil
}

View File

@@ -6,7 +6,6 @@ import (
"log"
"os"
"strconv"
"strings"
"testing"
"time"
@@ -633,9 +632,7 @@ func TestFQueryTriggerOrderHistory(t *testing.T) {
func TestFetchTradablePairs(t *testing.T) {
t.Parallel()
_, err := h.FetchTradablePairs(context.Background(), asset.Futures)
if err != nil {
t.Error(err)
}
require.NoError(t, err)
}
func TestUpdateTickerSpot(t *testing.T) {
@@ -2674,49 +2671,33 @@ func TestGetAvailableTransferChains(t *testing.T) {
t.Error("expected more than one result")
}
}
func TestFormatFuturesPair(t *testing.T) {
r, err := h.formatFuturesPair(futuresTestPair, false)
if err != nil {
t.Error(err)
}
if r != "BTC_CW" {
t.Errorf("expected BTC_CW, got %s", r)
}
availInstruments, err := h.FetchTradablePairs(context.Background(), asset.Futures)
if err != nil {
t.Error(err)
}
if len(availInstruments) == 0 {
t.Error("expected instruments, got 0")
}
// test getting a tradable pair in the format of BTC210827 but make it lower
// case to test correct formatting
r, err = h.formatFuturesPair(availInstruments[0], false)
if err != nil {
t.Error(err)
}
require.NoError(t, err)
assert.Equal(t, "BTC_CW", r)
p, err := h.FetchTradablePairs(context.Background(), asset.Futures)
require.NoError(t, err, "FetchTradablePairs must not error")
require.NotEmpty(t, p, "FetchTradablePairs must return pairs")
// test getting a tradable pair in the format of BTC210827 but make it lower case to test correct formatting
r, err = h.formatFuturesPair(p[0].Lower(), false)
require.NoError(t, err)
assert.Len(t, r, 9, "Should be an 9 character string")
// Test for upper case 'BTC' not lower case 'btc', disregarded numerals
// as they not deterministic from this endpoint.
if !strings.Contains(r, "BTC") {
t.Errorf("expected %s, got %s", "BTC220708", r)
}
assert.Equal(t, "BTC", r[0:3])
r, err = h.formatFuturesPair(futuresTestPair, true)
if err != nil {
t.Error(err)
}
if r == "BTC_CW" {
t.Errorf("expected BTC{{date}}, got %s", r)
}
require.NoError(t, err)
assert.Len(t, r, 9, "Should be an 9 character string")
assert.Equal(t, "BTC", r[0:3])
r, err = h.formatFuturesPair(currency.NewPair(currency.BTC, currency.USDT), false)
if err != nil {
t.Error(err)
}
if r != "BTC-USDT" {
t.Errorf("expected BTC-USDT, got %s", r)
}
require.NoError(t, err)
assert.Equal(t, "BTC-USDT", r)
}
func TestSearchForExistedWithdrawsAndDeposits(t *testing.T) {
@@ -2870,74 +2851,52 @@ func TestUpdateTickers(t *testing.T) {
t.Parallel()
for _, a := range h.GetAssetTypes(false) {
err := h.UpdateTickers(context.Background(), a)
assert.NoErrorf(t, err, "asset %s", a)
require.NoErrorf(t, err, "asset %s", a)
avail, err := h.GetAvailablePairs(a)
require.NoError(t, err)
for x := range avail {
_, err = ticker.GetTicker(h.Name, avail[x], a)
assert.NoError(t, err)
for _, p := range avail {
_, err = ticker.GetTicker(h.Name, p, a)
assert.NoErrorf(t, err, "Could not get ticker for %s %s", a, p)
}
}
}
func TestConvertContractShortHandToExpiry(t *testing.T) {
func TestPairFromContractExpiryCode(t *testing.T) {
t.Parallel()
tt := time.Now()
cp := currency.NewPair(currency.BTC, currency.NewCode("CW"))
cp, err := h.convertContractShortHandToExpiry(cp, tt)
assert.NoError(t, err)
assert.NotEqual(t, "CW", cp.Quote.String())
tick, err := h.FetchTicker(context.Background(), cp, asset.Futures)
if assert.NoError(t, err) {
assert.NotZero(t, tick.Close)
h := new(HUOBI) //nolint:govet // Intentional shadow to avoid future copy/paste mistakes
require.NoError(t, testexch.Setup(h), "Test Instance Setup must not fail")
_, err := h.FetchTradablePairs(context.Background(), asset.Futures)
require.NoError(t, err)
n := time.Now().Truncate(24 * time.Hour)
for _, cType := range contractExpiryNames {
p, err := h.pairFromContractExpiryCode(currency.Pair{
Base: currency.BTC,
Quote: currency.NewCode(cType),
})
if cType == "NQ" && err != nil {
continue // Next Quarter is intermittently present
}
require.NoErrorf(t, err, "pairFromContractExpiryCode must not error for %s code", cType)
assert.Equal(t, currency.BTC, p.Base, "pair Base should be the same")
h.futureContractCodesMutex.RLock()
exp, ok := h.futureContractCodes[cType]
h.futureContractCodesMutex.RUnlock()
require.True(t, ok, "%s type must be in contractExpiryNames", cType)
assert.Equal(t, currency.BTC, p.Base, "pair Base should be the same")
assert.Equal(t, exp, p.Quote, "pair Quote should be the same")
d, err := time.Parse("060102", p.Quote.String())
require.NoError(t, err, "currency code must be a parsable date")
require.Falsef(t, d.Before(n), "%s expiry must be today or after", cType)
switch cType {
case "CW", "NW":
require.True(t, d.Before(n.Add(24*time.Hour*14)), "%s expiry must be within 2 weeks", cType)
case "CQ", "NQ":
require.True(t, d.Before(n.Add(24*time.Hour*90*2)), "%s expiry must be within 2 quarters", cType)
}
}
cp = currency.NewPair(currency.BTC, currency.NewCode("NW"))
cp, err = h.convertContractShortHandToExpiry(cp, tt)
assert.NoError(t, err)
assert.NotEqual(t, "NW", cp.Quote.String())
tick, err = h.FetchTicker(context.Background(), cp, asset.Futures)
if assert.NoError(t, err) {
assert.NotZero(t, tick.Close)
}
cp = currency.NewPair(currency.BTC, currency.NewCode("CQ"))
cp, err = h.convertContractShortHandToExpiry(cp, tt)
assert.NoError(t, err)
assert.NotEqual(t, "CQ", cp.Quote.String())
tick, err = h.FetchTicker(context.Background(), cp, asset.Futures)
if assert.NoError(t, err) {
assert.NotZero(t, tick.Close)
}
// calculate a specific date
cp = currency.NewPair(currency.BTC, currency.NewCode("CQ"))
tt = time.Date(2021, 6, 3, 0, 0, 0, 0, time.UTC)
cp, err = h.convertContractShortHandToExpiry(cp, tt)
assert.NoError(t, err)
assert.Equal(t, "210625", cp.Quote.String())
cp = currency.NewPair(currency.BTC, currency.NewCode("CW"))
cp, err = h.convertContractShortHandToExpiry(cp, tt)
assert.NoError(t, err)
assert.Equal(t, "210604", cp.Quote.String())
cp = currency.NewPair(currency.BTC, currency.NewCode("CWif hat"))
_, err = h.convertContractShortHandToExpiry(cp, tt)
assert.ErrorIs(t, err, errInvalidContractType)
tt = time.Now()
cp = currency.NewPair(currency.BTC, currency.NewCode("NQ"))
cp, err = h.convertContractShortHandToExpiry(cp, tt)
assert.NoError(t, err)
assert.NotEqual(t, "NQ", cp.Quote.String())
tick, err = h.FetchTicker(context.Background(), cp, asset.Futures)
if err != nil {
// Huobi doesn't always have a next-quarter contract, return if no data found
return
}
assert.NotZero(t, tick.Close)
}
func TestGetOpenInterest(t *testing.T) {
@@ -2948,7 +2907,6 @@ func TestGetOpenInterest(t *testing.T) {
Asset: asset.USDTMarginedFutures,
})
assert.ErrorIs(t, err, asset.ErrNotSupported)
resp, err := h.GetOpenInterest(context.Background(), key.PairAsset{
Base: currency.BTC.Item,
Quote: currency.USD.Item,
@@ -2964,7 +2922,6 @@ func TestGetOpenInterest(t *testing.T) {
})
assert.NoError(t, err)
assert.NotEmpty(t, resp)
resp, err = h.GetOpenInterest(context.Background())
assert.NoError(t, err)
assert.NotEmpty(t, resp)

View File

@@ -1166,13 +1166,14 @@ var (
"reduceShort": 12,
}
validContractTypes = []string{
"this_week", "next_week", "quarter", "next_quarter",
contractExpiryNames = map[string]string{
"this_week": "CW",
"next_week": "NW",
"quarter": "CQ",
"next_quarter": "NQ",
}
validContractShortTypes = []string{
"cw", "nw", "cq", "nq",
}
validContractExpiryCodes = []string{"CW", "NW", "CQ", "NQ"}
validFuturesPeriods = []string{
"1min", "5min", "15min", "30min", "60min", "1hour", "4hour", "1day",

View File

@@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"slices"
"sort"
"strconv"
"strings"
@@ -244,7 +245,6 @@ func (h *HUOBI) FetchTradablePairs(ctx context.Context, a asset.Item) (currency.
}
var pairs []currency.Pair
var pair currency.Pair
switch a {
case asset.Spot:
symbols, err := h.GetSymbols(ctx)
@@ -258,7 +258,7 @@ func (h *HUOBI) FetchTradablePairs(ctx context.Context, a asset.Item) (currency.
continue
}
pair, err = currency.NewPairFromStrings(symbols[x].BaseCurrency,
pair, err := currency.NewPairFromStrings(symbols[x].BaseCurrency,
symbols[x].QuoteCurrency)
if err != nil {
return nil, err
@@ -288,16 +288,30 @@ func (h *HUOBI) FetchTradablePairs(ctx context.Context, a asset.Item) (currency.
return nil, err
}
pairs = make([]currency.Pair, 0, len(symbols.Data))
for c := range symbols.Data {
if symbols.Data[c].ContractStatus != 1 {
expiryCodeDates := map[string]currency.Code{}
for _, c := range symbols.Data {
if c.ContractStatus != 1 {
continue
}
pair, err := currency.NewPairFromString(symbols.Data[c].ContractCode)
pair, err := currency.NewPairFromString(c.ContractCode)
if err != nil {
return nil, err
}
pairs = append(pairs, pair)
if cType, ok := contractExpiryNames[c.ContractType]; ok {
if v, ok := expiryCodeDates[cType]; !ok {
expiryCodeDates[cType] = currency.NewCode(pair.Quote.String())
} else if v.String() != pair.Quote.String() {
return nil, fmt.Errorf("%w: %s (%s vs %s)", errInconsistentContractExpiry, cType, v.String(), pair.Quote.String())
}
}
}
// We cache contract expiries on the exchange locally right now because there's no exchange base holder for them
// It's not as dangerous as it seems, because when contracts change, so would tradeable pairs,
// so by caching them in FetchTradablePairs we're not adding any extra-layer of out-of-date data
h.futureContractCodesMutex.Lock()
h.futureContractCodes = expiryCodeDates
h.futureContractCodesMutex.Unlock()
}
return pairs, nil
}
@@ -321,6 +335,7 @@ func (h *HUOBI) UpdateTradablePairs(ctx context.Context, forceUpdate bool) error
// UpdateTickers updates the ticker for all currency pairs of a given asset type
func (h *HUOBI) UpdateTickers(ctx context.Context, a asset.Item) error {
var errs error
switch a {
case asset.Spot:
ticks, err := h.GetTickers(ctx)
@@ -331,10 +346,10 @@ func (h *HUOBI) UpdateTickers(ctx context.Context, a asset.Item) error {
var cp currency.Pair
cp, _, err = h.MatchSymbolCheckEnabled(ticks.Data[i].Symbol, a, false)
if err != nil {
if errors.Is(err, currency.ErrPairNotFound) {
continue
if !errors.Is(err, currency.ErrPairNotFound) {
errs = common.AppendError(errs, err)
}
return err
continue
}
err = ticker.ProcessTicker(&ticker.Price{
High: ticks.Data[i].High,
@@ -353,7 +368,7 @@ func (h *HUOBI) UpdateTickers(ctx context.Context, a asset.Item) error {
LastUpdated: time.Now(),
})
if err != nil {
return err
errs = common.AppendError(errs, err)
}
}
case asset.CoinMarginedFutures:
@@ -365,10 +380,10 @@ func (h *HUOBI) UpdateTickers(ctx context.Context, a asset.Item) error {
var cp currency.Pair
cp, _, err = h.MatchSymbolCheckEnabled(ticks[i].ContractCode, a, true)
if err != nil {
if errors.Is(err, currency.ErrPairNotFound) {
continue
if !errors.Is(err, currency.ErrPairNotFound) {
errs = common.AppendError(errs, err)
}
return err
continue
}
tt := time.UnixMilli(ticks[i].Timestamp)
err = ticker.ProcessTicker(&ticker.Price{
@@ -388,73 +403,67 @@ func (h *HUOBI) UpdateTickers(ctx context.Context, a asset.Item) error {
LastUpdated: tt,
})
if err != nil {
return err
errs = common.AppendError(errs, err)
}
}
case asset.Futures:
linearTicks, err := h.GetBatchLinearSwapContracts(ctx)
if err != nil {
return err
ticks := []FuturesBatchTicker{}
// TODO: Linear swap contracts are coin-m assets
if coinMTicks, err := h.GetBatchLinearSwapContracts(ctx); err != nil {
errs = common.AppendError(errs, err)
} else {
ticks = append(ticks, coinMTicks...)
}
ticks, err := h.GetBatchFuturesContracts(ctx)
if err != nil {
return err
if futureTicks, err := h.GetBatchFuturesContracts(ctx); err != nil {
errs = common.AppendError(errs, err)
} else {
ticks = append(ticks, futureTicks...)
}
allTicks := make([]FuturesBatchTicker, 0, len(linearTicks)+len(ticks))
allTicks = append(allTicks, linearTicks...)
allTicks = append(allTicks, ticks...)
for i := range allTicks {
for i := range ticks {
var cp currency.Pair
if allTicks[i].Symbol != "" {
cp, err = currency.NewPairFromString(allTicks[i].Symbol)
if err != nil {
return err
var err error
if ticks[i].Symbol != "" {
cp, err = currency.NewPairFromString(ticks[i].Symbol)
if err == nil {
cp, err = h.pairFromContractExpiryCode(cp)
}
cp, err = h.convertContractShortHandToExpiry(cp, time.Now())
if err != nil {
return err
}
cp, _, err = h.MatchSymbolCheckEnabled(cp.String(), a, true)
if err != nil {
if errors.Is(err, currency.ErrPairNotFound) {
continue
}
return err
if err == nil {
cp, _, err = h.MatchSymbolCheckEnabled(cp.String(), a, true)
}
} else {
cp, _, err = h.MatchSymbolCheckEnabled(allTicks[i].ContractCode, a, true)
if err != nil {
if errors.Is(err, currency.ErrPairNotFound) {
continue
}
return err
}
cp, _, err = h.MatchSymbolCheckEnabled(ticks[i].ContractCode, a, true)
}
tt := time.UnixMilli(allTicks[i].Timestamp)
if err != nil {
if !errors.Is(err, currency.ErrPairNotFound) {
errs = common.AppendError(errs, err)
}
continue
}
tt := time.UnixMilli(ticks[i].Timestamp)
err = ticker.ProcessTicker(&ticker.Price{
High: allTicks[i].High.Float64(),
Low: allTicks[i].Low.Float64(),
Volume: allTicks[i].Volume.Float64(),
QuoteVolume: allTicks[i].Amount.Float64(),
Open: allTicks[i].Open.Float64(),
Close: allTicks[i].Close.Float64(),
Bid: allTicks[i].Bid[0],
BidSize: allTicks[i].Bid[1],
Ask: allTicks[i].Ask[0],
AskSize: allTicks[i].Ask[1],
High: ticks[i].High.Float64(),
Low: ticks[i].Low.Float64(),
Volume: ticks[i].Volume.Float64(),
QuoteVolume: ticks[i].Amount.Float64(),
Open: ticks[i].Open.Float64(),
Close: ticks[i].Close.Float64(),
Bid: ticks[i].Bid[0],
BidSize: ticks[i].Bid[1],
Ask: ticks[i].Ask[0],
AskSize: ticks[i].Ask[1],
Pair: cp,
ExchangeName: h.Name,
AssetType: a,
LastUpdated: tt,
})
if err != nil {
return err
errs = common.AppendError(errs, err)
}
}
default:
return fmt.Errorf("%w %v", asset.ErrNotSupported, a)
}
return nil
return errs
}
// UpdateTicker updates and returns the ticker for a currency pair
@@ -2329,8 +2338,7 @@ func (h *HUOBI) GetOpenInterest(ctx context.Context, k ...key.PairAsset) ([]futu
if len(k) == 1 {
switch k[0].Asset {
case asset.Futures:
_, err := strconv.ParseInt(k[0].Quote.Symbol, 10, 64)
if err == nil {
if !slices.Contains(validContractExpiryCodes, strings.ToUpper(k[0].Pair().Quote.String())) {
// Huobi does not like requests being made with contract expiry in them (eg BTC240109)
return nil, fmt.Errorf("%w %v, must use shorthand such as CW (current week)", currency.ErrCurrencyNotSupported, k[0].Pair())
}