exchanges: Refactor time handling and other minor improvements (#1948)

* exchanges: Refactor time handling and other minor improvements

- Updated Kraken wrapper to utilise new time handling methods.
- Simplified Kucoin types by removing unnecessary structures and using direct JSON unmarshalling.
- Improved websocket handling in Kucoin to directly parse candlestick data.
- Modified Lbank types to use the new time representation.
- Adjusted Poloniex wrapper and types to utilise the new time handling.
- Updated Yobit types and wrapper to reflect changes in time representation.
- Introduced DateTime type for better handling of specific time formats.
- Added tests for DateTime unmarshalling to ensure correctness.
- Rid UTC().Unix and UTC().UnixMilli as it's not needed
- Correct Huobi timestamp usage for some endpoints.
- Rid RFC3339 time parsing since Go does that automatically.

* exchanges: Refactor JSON unmarshalling for various types and improve test coverage

* linter: Update error message in TestGetKlines

* refactor: Simplify JSON unmarshalling in MovementHistory and improve test assertions in GetKlines

* refactor: Improve JSON unmarshalling for channel name and clarify comment in wsProcessOpenOrders

* refactor: Update time handling in Huobi types to use types.Time for createdAt fields and relax GetLiquidationOrders test

* refactor: Move wsTicker, wsSpread, wsTrades, and wsCandle types to kraken_types.go for better organistion

* refactor: Add validation for underlying parameter in GetExpirationTime and update tests
This commit is contained in:
Adrian Gallagher
2025-07-01 09:11:55 +10:00
committed by GitHub
parent 48a66c9faa
commit 3cc9a2b9e0
92 changed files with 2488 additions and 3276 deletions

View File

@@ -253,13 +253,13 @@ type SwapWsSubLiquidationOrders struct {
Topic string `json:"topic"`
Timestamp types.Time `json:"ts"`
OrdersData []struct {
Symbol string `json:"symbol"`
ContractCode string `json:"contract_code"`
Direction string `json:"direction"`
Offset string `json:"offset"`
Volume float64 `json:"volume"`
Price float64 `json:"price"`
CreatedAt int64 `json:"created_at"`
Symbol string `json:"symbol"`
ContractCode string `json:"contract_code"`
Direction string `json:"direction"`
Offset string `json:"offset"`
Volume float64 `json:"volume"`
Price float64 `json:"price"`
CreatedAt types.Time `json:"created_at"`
} `json:"data"`
}
@@ -269,13 +269,13 @@ type SwapWsSubFundingData struct {
Topic string `json:"topic"`
Timestamp types.Time `json:"ts"`
FundingData []struct {
Symbol string `json:"symbol"`
ContractCode string `json:"contract_code"`
FeeAsset string `json:"fee_asset"`
FundingTime int64 `json:"funding_time,string"`
FundingRate float64 `json:"funding_rate,string"`
EstimatedRate float64 `json:"estimated_rate,string"`
SettlementTime int64 `json:"settlement_time,string"`
Symbol string `json:"symbol"`
ContractCode string `json:"contract_code"`
FeeAsset string `json:"fee_asset"`
FundingTime types.Time `json:"funding_time"`
FundingRate float64 `json:"funding_rate,string"`
EstimatedRate float64 `json:"estimated_rate,string"`
SettlementTime types.Time `json:"settlement_time"`
} `json:"data"`
}
@@ -435,7 +435,7 @@ type CoinMarginedFuturesTrade struct {
// InsuranceAndClawbackData stores insurance fund's and clawback rate's data
type InsuranceAndClawbackData struct {
Timestamp string `json:"timestamp"`
Timestamp types.Time `json:"timestamp"`
Data []struct {
ContractCode string `json:"contract_code"`
InsuranceFund float64 `json:"insurance_fund"`
@@ -533,15 +533,15 @@ type TraderSentimentIndexPositionData struct {
// LiquidationOrdersData stores data of liquidation orders
type LiquidationOrdersData struct {
Data []struct {
QueryID int64 `json:"query_id"`
ContractCode string `json:"contract_code"`
Symbol string `json:"symbol"`
Direction string `json:"direction"`
Offset string `json:"offset"`
Volume float64 `json:"volume"`
Price float64 `json:"price"`
CreatedAt int64 `json:"created_at"`
Amount float64 `json:"amount"`
QueryID int64 `json:"query_id"`
ContractCode string `json:"contract_code"`
Symbol string `json:"symbol"`
Direction string `json:"direction"`
Offset string `json:"offset"`
Volume float64 `json:"volume"`
Price float64 `json:"price"`
CreatedAt types.Time `json:"created_at"`
Amount float64 `json:"amount"`
} `json:"data"`
}
@@ -553,13 +553,13 @@ type SwapFundingRatesResponse struct {
// FundingRatesData stores funding rates data
type FundingRatesData struct {
EstimatedRate float64 `json:"estimated_rate,string"`
FundingRate float64 `json:"funding_rate,string"`
ContractCode string `json:"contractCode"`
Symbol string `json:"symbol"`
FeeAsset string `json:"fee_asset"`
FundingTime int64 `json:"fundingTime,string"`
NextFundingTime int64 `json:"next_funding_time,string"`
EstimatedRate float64 `json:"estimated_rate,string"`
FundingRate float64 `json:"funding_rate,string"`
ContractCode string `json:"contractCode"`
Symbol string `json:"symbol"`
FeeAsset string `json:"fee_asset"`
FundingTime types.Time `json:"fundingTime"`
NextFundingTime types.Time `json:"next_funding_time"`
}
// HistoricalFundingRateData stores historical funding rates for perpetuals
@@ -574,13 +574,13 @@ type HistoricalFundingRateData struct {
// HistoricalRateData stores historical rates data
type HistoricalRateData struct {
FundingRate float64 `json:"funding_rate,string"`
RealizedRate float64 `json:"realized_rate,string"`
FundingTime int64 `json:"fundingTime,string"`
ContractCode string `json:"contract_code"`
Symbol string `json:"symbol"`
FeeAsset string `json:"fee_asset"`
AvgPremiumIndex float64 `json:"avg_premium_index,string"`
FundingRate float64 `json:"funding_rate,string"`
RealizedRate float64 `json:"realized_rate,string"`
FundingTime types.Time `json:"fundingTime"`
ContractCode string `json:"contract_code"`
Symbol string `json:"symbol"`
FeeAsset string `json:"fee_asset"`
AvgPremiumIndex float64 `json:"avg_premium_index,string"`
}
// PremiumIndexKlineData stores kline data for premium

View File

@@ -264,13 +264,13 @@ type FTopPositionsLongShortRatio struct {
type FLiquidationOrdersInfo struct {
Data struct {
Orders []struct {
Symbol string `json:"symbol"`
ContractCode string `json:"contract_code"`
Direction string `json:"direction"`
Offset string `json:"offset"`
Volume float64 `json:"volume"`
Price float64 `json:"price"`
CreatedAt int64 `json:"created_at"`
Symbol string `json:"symbol"`
ContractCode string `json:"contract_code"`
Direction string `json:"direction"`
Offset string `json:"offset"`
Volume float64 `json:"volume"`
Price float64 `json:"price"`
CreatedAt types.Time `json:"created_at"`
} `json:"orders"`
TotalPage int64 `json:"total_page"`
CurrentPage int64 `json:"current_page"`
@@ -427,16 +427,16 @@ type FFinancialRecords struct {
type FSettlementRecords struct {
Data struct {
SettlementRecords []struct {
Symbol string `json:"symbol"`
MarginBalanceInit float64 `json:"margin_balance_init"`
MarginBalance int64 `json:"margin_balance"`
SettlementProfitReal float64 `json:"settlement_profit_real"`
SettlementTime int64 `json:"settlement_time"`
Clawback float64 `json:"clawback"`
DeliveryFee float64 `json:"delivery_fee"`
OffsetProfitLoss float64 `json:"offset_profitloss"`
Fee float64 `json:"fee"`
FeeAsset string `json:"fee_asset"`
Symbol string `json:"symbol"`
MarginBalanceInit float64 `json:"margin_balance_init"`
MarginBalance int64 `json:"margin_balance"`
SettlementProfitReal float64 `json:"settlement_profit_real"`
SettlementTime types.Time `json:"settlement_time"`
Clawback float64 `json:"clawback"`
DeliveryFee float64 `json:"delivery_fee"`
OffsetProfitLoss float64 `json:"offset_profitloss"`
Fee float64 `json:"fee"`
FeeAsset string `json:"fee_asset"`
Positions []struct {
Symbol string `json:"symbol"`
ContractCode string `json:"contract_code"`
@@ -725,29 +725,29 @@ type FOpenOrdersData struct {
type FOrderHistoryData struct {
Data struct {
Orders []struct {
Symbol string `json:"symbol"`
ContractType string `json:"contract_type"`
ContractCode string `json:"contract_code"`
Volume float64 `json:"volume"`
Price float64 `json:"price"`
OrderPriceType string `json:"order_price_type"`
Direction string `json:"direction"`
Offset string `json:"offset"`
LeverageRate float64 `json:"lever_rate"`
OrderID int64 `json:"order_id"`
OrderIDString string `json:"order_id_str"`
OrderSource string `json:"order_source"`
CreateDate int64 `json:"create_date"`
TradeVolume float64 `json:"trade_volume"`
TradeTurnover float64 `json:"trade_turnover"`
Fee float64 `json:"fee"`
TradeAvgPrice float64 `json:"trade_avg_price"`
MarginFrozen float64 `json:"margin_frozen"`
Profit float64 `json:"profit"`
Status int64 `json:"status"`
OrderType int64 `json:"order_type"`
FeeAsset string `json:"fee_asset"`
LiquidationType int64 `json:"liquidation_type"`
Symbol string `json:"symbol"`
ContractType string `json:"contract_type"`
ContractCode string `json:"contract_code"`
Volume float64 `json:"volume"`
Price float64 `json:"price"`
OrderPriceType string `json:"order_price_type"`
Direction string `json:"direction"`
Offset string `json:"offset"`
LeverageRate float64 `json:"lever_rate"`
OrderID int64 `json:"order_id"`
OrderIDString string `json:"order_id_str"`
OrderSource string `json:"order_source"`
CreateDate types.Time `json:"create_date"`
TradeVolume float64 `json:"trade_volume"`
TradeTurnover float64 `json:"trade_turnover"`
Fee float64 `json:"fee"`
TradeAvgPrice float64 `json:"trade_avg_price"`
MarginFrozen float64 `json:"margin_frozen"`
Profit float64 `json:"profit"`
Status int64 `json:"status"`
OrderType int64 `json:"order_type"`
FeeAsset string `json:"fee_asset"`
LiquidationType int64 `json:"liquidation_type"`
} `json:"orders"`
TotalPage int64 `json:"total_page"`
CurrentPage int64 `json:"current_page"`

View File

@@ -20,6 +20,7 @@ import (
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"github.com/thrasher-corp/gocryptotrader/types"
)
const (
@@ -366,13 +367,13 @@ func (h *HUOBI) GetCurrenciesIncludingChains(ctx context.Context, curr currency.
func (h *HUOBI) GetCurrentServerTime(ctx context.Context) (time.Time, error) {
var result struct {
Response
Timestamp int64 `json:"data"`
Timestamp types.Time `json:"data"`
}
err := h.SendHTTPRequest(ctx, exchange.RestSpot, "/v"+huobiAPIVersion+"/"+huobiTimestamp, &result)
if result.ErrorMessage != "" {
return time.Time{}, errors.New(result.ErrorMessage)
}
return time.UnixMilli(result.Timestamp), err
return result.Timestamp.Time(), err
}
// GetAccounts returns the Huobi user accounts

View File

@@ -282,7 +282,7 @@ func (h *HUOBI) GetTraderSentimentIndexPosition(ctx context.Context, code curren
}
// GetLiquidationOrders gets liquidation orders for a given perp
func (h *HUOBI) GetLiquidationOrders(ctx context.Context, contract currency.Pair, tradeType string, startTime, endTime int64, direction string, fromID int64) (LiquidationOrdersData, error) {
func (h *HUOBI) GetLiquidationOrders(ctx context.Context, contract currency.Pair, tradeType string, startTime, endTime time.Time, direction string, fromID int64) (LiquidationOrdersData, error) {
var resp LiquidationOrdersData
formattedContract, err := h.FormatSymbol(contract, asset.CoinMarginedFutures)
if err != nil {
@@ -296,11 +296,11 @@ func (h *HUOBI) GetLiquidationOrders(ctx context.Context, contract currency.Pair
params.Set("contract", formattedContract)
params.Set("trade_type", strconv.FormatInt(tType, 10))
if startTime != 0 {
params.Set("start_time", strconv.FormatInt(startTime, 10))
if !startTime.IsZero() {
params.Set("start_time", strconv.FormatInt(startTime.UnixMilli(), 10))
}
if endTime != 0 {
params.Set("end_time", strconv.FormatInt(startTime, 10))
if !endTime.IsZero() {
params.Set("end_time", strconv.FormatInt(endTime.UnixMilli(), 10))
}
if direction != "" {
params.Set("direct", direction)
@@ -512,8 +512,8 @@ func (h *HUOBI) GetSwapSettlementRecords(ctx context.Context, code currency.Pair
if startTime.After(endTime) {
return resp, errors.New("startTime cannot be after endTime")
}
req["start_time"] = strconv.FormatInt(startTime.Unix(), 10)
req["end_time"] = strconv.FormatInt(endTime.Unix(), 10)
req["start_time"] = strconv.FormatInt(startTime.UnixMilli(), 10)
req["end_time"] = strconv.FormatInt(endTime.UnixMilli(), 10)
}
if pageIndex != 0 {
req["page_index"] = pageIndex

View File

@@ -521,8 +521,9 @@ func TestGetSwapMarketDepth(t *testing.T) {
func TestGetSwapKlineData(t *testing.T) {
t.Parallel()
_, err := h.GetSwapKlineData(t.Context(), btcusdPair, "5min", 5, time.Now().Add(-time.Hour), time.Now())
r, err := h.GetSwapKlineData(t.Context(), btcusdPair, "5min", 5, time.Now().Add(-time.Hour), time.Now())
require.NoError(t, err)
assert.NotEmpty(t, r.Data, "GetSwapKlineData should return some data")
}
func TestGetSwapMarketOverview(t *testing.T) {
@@ -570,8 +571,8 @@ func TestGetTraderSentimentIndexPosition(t *testing.T) {
func TestGetLiquidationOrders(t *testing.T) {
t.Parallel()
_, err := h.GetLiquidationOrders(t.Context(), btcusdPair, "closed", 0, 0, "", 0)
require.NoError(t, err)
_, err := h.GetLiquidationOrders(t.Context(), btcusdPair, "closed", time.Now().AddDate(0, 0, -2), time.Now(), "", 0)
assert.NoError(t, err, "GetLiquidationOrders should not error")
}
func TestGetHistoricalFundingRates(t *testing.T) {
@@ -662,8 +663,9 @@ func TestGetAccountFinancialRecords(t *testing.T) {
func TestGetSwapSettlementRecords(t *testing.T) {
t.Parallel()
sharedtestvalues.SkipTestIfCredentialsUnset(t, h)
_, err := h.GetSwapSettlementRecords(t.Context(), ethusdPair, time.Time{}, time.Time{}, 0, 0)
r, err := h.GetSwapSettlementRecords(t.Context(), ethusdPair, time.Now().AddDate(0, -1, 0), time.Now(), 0, 0)
require.NoError(t, err)
assert.NotEmpty(t, r.Data, "GetSwapSettlementRecords should return some data")
}
func TestGetAvailableLeverage(t *testing.T) {

View File

@@ -379,13 +379,13 @@ type FWsSubLiquidationOrders struct {
Topic string `json:"topic"`
Timestamp types.Time `json:"ts"`
OrdersData []struct {
Symbol string `json:"symbol"`
ContractCode string `json:"contract_code"`
Direction string `json:"direction"`
Offset string `json:"offset"`
Volume float64 `json:"volume"`
Price float64 `json:"price"`
CreatedAt int64 `json:"created_at"`
Symbol string `json:"symbol"`
ContractCode string `json:"contract_code"`
Direction string `json:"direction"`
Offset string `json:"offset"`
Volume float64 `json:"volume"`
Price float64 `json:"price"`
CreatedAt types.Time `json:"created_at"`
} `json:"data"`
}
@@ -475,14 +475,14 @@ type ResponseV2 struct {
// SwapMarketsData stores market data for swaps
type SwapMarketsData struct {
Symbol string `json:"symbol"`
ContractCode string `json:"contract_code"`
ContractSize float64 `json:"contract_size"`
PriceTick float64 `json:"price_tick"`
SettlementDate string `json:"settlement_date"`
CreateDate string `json:"create_date"`
DeliveryTime string `json:"delivery_time"`
ContractStatus int64 `json:"contract_status"`
Symbol string `json:"symbol"`
ContractCode string `json:"contract_code"`
ContractSize float64 `json:"contract_size"`
PriceTick float64 `json:"price_tick"`
SettlementDate types.Time `json:"settlement_date"`
CreateDate string `json:"create_date"`
DeliveryTime types.Time `json:"delivery_time"`
ContractStatus int64 `json:"contract_status"`
}
// KlineItem stores a kline item
@@ -580,7 +580,7 @@ type OrderBookDataRequestParams struct {
// Orderbook stores the orderbook data
type Orderbook struct {
ID int64 `json:"id"`
Timetstamp int64 `json:"ts"`
Timetstamp types.Time `json:"ts"`
Bids [][2]float64 `json:"bids"`
Asks [][2]float64 `json:"asks"`
}
@@ -678,39 +678,39 @@ type CancelOrderBatch struct {
// OrderInfo stores the order info
type OrderInfo struct {
ID int64 `json:"id"`
Symbol string `json:"symbol"`
AccountID int64 `json:"account-id"`
Amount float64 `json:"amount,string"`
Price float64 `json:"price,string"`
CreatedAt int64 `json:"created-at"`
Type string `json:"type"`
FieldAmount float64 `json:"field-amount,string"`
FieldCashAmount float64 `json:"field-cash-amount,string"`
FilledAmount float64 `json:"filled-amount,string"`
FilledCashAmount float64 `json:"filled-cash-amount,string"`
FilledFees float64 `json:"filled-fees,string"`
FinishedAt int64 `json:"finished-at"`
UserID int64 `json:"user-id"`
Source string `json:"source"`
State string `json:"state"`
CanceledAt int64 `json:"canceled-at"`
Exchange string `json:"exchange"`
Batch string `json:"batch"`
ID int64 `json:"id"`
Symbol string `json:"symbol"`
AccountID int64 `json:"account-id"`
Amount float64 `json:"amount,string"`
Price float64 `json:"price,string"`
CreatedAt types.Time `json:"created-at"`
Type string `json:"type"`
FieldAmount float64 `json:"field-amount,string"`
FieldCashAmount float64 `json:"field-cash-amount,string"`
FilledAmount float64 `json:"filled-amount,string"`
FilledCashAmount float64 `json:"filled-cash-amount,string"`
FilledFees float64 `json:"filled-fees,string"`
FinishedAt types.Time `json:"finished-at"`
UserID int64 `json:"user-id"`
Source string `json:"source"`
State string `json:"state"`
CanceledAt int64 `json:"canceled-at"`
Exchange string `json:"exchange"`
Batch string `json:"batch"`
}
// OrderMatchInfo stores the order match info
type OrderMatchInfo struct {
ID int `json:"id"`
OrderID int `json:"order-id"`
MatchID int `json:"match-id"`
Symbol string `json:"symbol"`
Type string `json:"type"`
Source string `json:"source"`
Price string `json:"price"`
FilledAmount string `json:"filled-amount"`
FilledFees string `json:"filled-fees"`
CreatedAt int64 `json:"created-at"`
ID int `json:"id"`
OrderID int `json:"order-id"`
MatchID int `json:"match-id"`
Symbol string `json:"symbol"`
Type string `json:"type"`
Source string `json:"source"`
Price string `json:"price"`
FilledAmount string `json:"filled-amount"`
FilledFees string `json:"filled-fees"`
CreatedAt types.Time `json:"created-at"`
}
// MarginOrder stores the margin order info

View File

@@ -1341,7 +1341,7 @@ func (h *HUOBI) GetOrderInfo(ctx context.Context, orderID string, pair currency.
Pair: p,
Type: orderType,
Side: orderSide,
Date: time.UnixMilli(respData.CreatedAt),
Date: respData.CreatedAt.Time(),
Status: orderStatus,
Price: respData.Price,
Amount: respData.Amount,
@@ -1514,7 +1514,7 @@ func (h *HUOBI) GetActiveOrders(ctx context.Context, req *order.MultiOrderReques
RemainingAmount: resp[x].Amount - resp[x].FilledAmount,
Pair: req.Pairs[i],
Exchange: h.Name,
Date: time.UnixMilli(resp[x].CreatedAt),
Date: resp[x].CreatedAt.Time(),
AccountID: strconv.FormatInt(resp[x].AccountID, 10),
Fee: resp[x].FilledFees,
}
@@ -1648,8 +1648,8 @@ func (h *HUOBI) GetOrderHistory(ctx context.Context, req *order.MultiOrderReques
CostAsset: req.Pairs[i].Quote,
Pair: req.Pairs[i],
Exchange: h.Name,
Date: time.UnixMilli(resp[x].CreatedAt),
CloseTime: time.UnixMilli(resp[x].FinishedAt),
Date: resp[x].CreatedAt.Time(),
CloseTime: resp[x].FinishedAt.Time(),
AccountID: strconv.FormatInt(resp[x].AccountID, 10),
Fee: resp[x].FilledFees,
}
@@ -1738,8 +1738,6 @@ func (h *HUOBI) GetOrderHistory(ctx context.Context, req *order.MultiOrderReques
if req.Type != orderVars.OrderType {
continue
}
orderCreateTime := time.Unix(openOrders.Data.Orders[x].CreateDate, 0)
p, err := currency.NewPairFromString(openOrders.Data.Orders[x].ContractCode)
if err != nil {
return orders, err
@@ -1759,7 +1757,7 @@ func (h *HUOBI) GetOrderHistory(ctx context.Context, req *order.MultiOrderReques
Type: orderVars.OrderType,
Status: orderVars.Status,
Pair: p,
Date: orderCreateTime,
Date: openOrders.Data.Orders[x].CreateDate.Time(),
})
}
currentPage++
@@ -2191,9 +2189,7 @@ func (h *HUOBI) GetLatestFundingRates(ctx context.Context, r *fundingrate.Latest
if !isPerp {
continue
}
var ft, nft time.Time
nft = time.UnixMilli(rates[i].NextFundingTime)
ft = time.UnixMilli(rates[i].FundingTime)
ft, nft := rates[i].FundingTime.Time(), rates[i].NextFundingTime.Time()
var fri time.Duration
if len(h.Features.Supports.FuturesCapabilities.SupportedFundingRateFrequencies) == 1 {
// can infer funding rate interval from the only funding rate frequency defined
@@ -2201,7 +2197,7 @@ func (h *HUOBI) GetLatestFundingRates(ctx context.Context, r *fundingrate.Latest
fri = k.Duration()
}
}
if rates[i].FundingTime == 0 {
if rates[i].FundingTime.Time().IsZero() {
ft = nft.Add(-fri)
}
if ft.After(time.Now()) {