mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-13 23:16:45 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user