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

@@ -58,7 +58,6 @@ const (
bitstampRateInterval = time.Minute * 10
bitstampRequestRate = 8000
bitstampTimeLayout = "2006-1-2 15:04:05"
)
// Bitstamp is the overarching type across the bitstamp package
@@ -260,53 +259,14 @@ func (b *Bitstamp) GetBalance(ctx context.Context) (Balances, error) {
// GetUserTransactions returns an array of transactions
func (b *Bitstamp) GetUserTransactions(ctx context.Context, currencyPair string) ([]UserTransactions, error) {
type Response struct {
Date string `json:"datetime"`
TransactionID int64 `json:"id"`
Type int64 `json:"type,string"`
USD types.Number `json:"usd"`
EUR types.Number `json:"eur"`
XRP types.Number `json:"xrp"`
BTC types.Number `json:"btc"`
BTCUSD types.Number `json:"btc_usd"`
Fee float64 `json:"fee,string"`
OrderID int64 `json:"order_id"`
}
var response []Response
var resp []UserTransactions
var err error
if currencyPair == "" {
if err := b.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, bitstampAPIUserTransactions,
true,
url.Values{},
&response); err != nil {
return nil, err
}
err = b.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, bitstampAPIUserTransactions, true, url.Values{}, &resp)
} else {
if err := b.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, bitstampAPIUserTransactions+"/"+currencyPair,
true,
url.Values{},
&response); err != nil {
return nil, err
}
err = b.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, bitstampAPIUserTransactions+"/"+currencyPair, true, url.Values{}, &resp)
}
transactions := make([]UserTransactions, len(response))
for x := range response {
transactions[x] = UserTransactions{
Date: response[x].Date,
TransactionID: response[x].TransactionID,
Type: response[x].Type,
EUR: response[x].EUR.Float64(),
XRP: response[x].XRP.Float64(),
USD: response[x].USD.Float64(),
BTC: response[x].BTC.Float64(),
BTCUSD: response[x].BTCUSD.Float64(),
Fee: response[x].Fee,
OrderID: response[x].OrderID,
}
}
return transactions, nil
return resp, err
}
// GetOpenOrders returns all open orders on the exchange
@@ -635,10 +595,6 @@ func (b *Bitstamp) SendAuthenticatedHTTPRequest(ctx context.Context, ep exchange
return json.Unmarshal(interim, result)
}
func parseTime(dateTime string) (time.Time, error) {
return time.Parse(bitstampTimeLayout, dateTime)
}
func filterOrderbookZeroBidPrice(ob *orderbook.Book) {
if len(ob.Bids) == 0 || ob.Bids[len(ob.Bids)-1].Price != 0 {
return

View File

@@ -364,11 +364,11 @@ func TestGetOpenOrders(t *testing.T) {
if mockTests {
assert.NotEmpty(t, o, "Orders should not be empty")
for _, res := range o {
assert.Equal(t, "2022-01-31 14:43:15", res.DateTime, "DateTime should match")
assert.Equal(t, time.Date(2022, 1, 31, 14, 43, 15, 0, time.UTC), res.DateTime.Time(), "DateTime should match")
assert.Equal(t, int64(1234123412341234), res.ID, "ID should match")
assert.Equal(t, 0.50000000, res.Amount, "Amount should match")
assert.Equal(t, 100.00, res.Price, "Price should match")
assert.Equal(t, 0, res.Type, "Type should match")
assert.Equal(t, int64(0), res.Type, "Type should match")
assert.Equal(t, 0.50000000, res.AmountAtCreate, "AmountAtCreate should match")
assert.Equal(t, 110.00, res.LimitPrice, "LimitPrice should match")
assert.Equal(t, "1234123412341234", res.ClientOrderID, "ClientOrderID should match")
@@ -388,14 +388,14 @@ func TestGetOrderStatus(t *testing.T) {
assert.ErrorContains(t, err, "Order not found")
} else {
require.NoError(t, err, "GetOrderStatus must not error")
assert.Equal(t, "2022-01-31 14:43:15", o.DateTime, "DateTime should match")
assert.Equal(t, time.Date(2022, 1, 31, 14, 43, 15, 0, time.UTC), o.DateTime.Time(), "DateTime should match")
assert.Equal(t, "1458532827766784", o.ID, "OrderID should match")
assert.Equal(t, 200.00, o.AmountRemaining, "AmountRemaining should match")
assert.Equal(t, int64(0), o.Type, "Type should match")
assert.Equal(t, "0.50000000", o.ClientOrderID, "ClientOrderID should match")
assert.Equal(t, "BTC/USD", o.Market, "Market should match")
for _, tr := range o.Transactions {
assert.Equal(t, "2022-01-31 14:43:15", tr.DateTime, "DateTime should match")
assert.Equal(t, time.Date(2022, 1, 31, 14, 43, 15, 0, time.UTC), tr.DateTime.Time(), "DateTime should match")
assert.Equal(t, 50.00, tr.Price, "Price should match")
assert.Equal(t, 101.00, tr.FromCurrency, "FromCurrency should match")
assert.Equal(t, 1.0, tr.ToCurrency, "ToCurrency should match")
@@ -417,7 +417,7 @@ func TestGetWithdrawalRequests(t *testing.T) {
for _, req := range r {
assert.Equal(t, int64(1), req.OrderID, "OrderId should match")
assert.Equal(t, "aMDHooGmAkyrsaQiKhAORhSNTmoRzxqWIO", req.Address, "Address should match")
assert.Equal(t, "2022-01-31 16:07:32", req.Date, "Date should match")
assert.Equal(t, time.Date(2022, 1, 31, 16, 7, 32, 0, time.UTC), req.Date.Time(), "Date should match")
assert.Equal(t, currency.BTC, req.Currency, "Currency should match")
assert.Equal(t, 0.00006000, req.Amount, "Amount should match")
assert.Equal(t, "NsOeFbQhRnpGzNIThWGBTkQwRJqTNOGPVhYavrVyMfkAyMUmIlUpFIwGTzSvpeOP", req.TransactionID, "TransactionID should match")
@@ -713,24 +713,6 @@ func TestGetDepositAddress(t *testing.T) {
assert.NotEmpty(t, a.Tag, "Tag should not be empty")
}
func TestParseTime(t *testing.T) {
t.Parallel()
tm, err := parseTime("2019-10-18 01:55:14")
if err != nil {
t.Error(err)
}
if tm.Year() != 2019 ||
tm.Month() != 10 ||
tm.Day() != 18 ||
tm.Hour() != 1 ||
tm.Minute() != 55 ||
tm.Second() != 14 {
t.Error("invalid time values")
}
}
func TestWsSubscription(t *testing.T) {
pressXToJSON := []byte(`{
"event": "bts:subscribe",

View File

@@ -23,18 +23,18 @@ const (
// Ticker holds ticker information
type Ticker struct {
Last float64 `json:"last,string"`
High float64 `json:"high,string"`
Low float64 `json:"low,string"`
Vwap float64 `json:"vwap,string"`
Volume float64 `json:"volume,string"`
Bid float64 `json:"bid,string"`
Ask float64 `json:"ask,string"`
Timestamp int64 `json:"timestamp,string"`
Open float64 `json:"open,string"`
Open24 float64 `json:"open_24,string"`
Side orderSide `json:"side,string"`
PercentChange24 float64 `json:"percent_change_24,string"`
Last float64 `json:"last,string"`
High float64 `json:"high,string"`
Low float64 `json:"low,string"`
Vwap float64 `json:"vwap,string"`
Volume float64 `json:"volume,string"`
Bid float64 `json:"bid,string"`
Ask float64 `json:"ask,string"`
Timestamp types.Time `json:"timestamp"`
Open float64 `json:"open,string"`
Open24 float64 `json:"open_24,string"`
Side orderSide `json:"side,string"`
PercentChange24 float64 `json:"percent_change_24,string"`
}
// OrderbookBase holds singular price information
@@ -63,11 +63,11 @@ type TradingPair struct {
// Transactions holds transaction data
type Transactions struct {
Date int64 `json:"date,string"`
TradeID int64 `json:"tid,string"`
Price float64 `json:"price,string"`
Type int `json:"type,string"`
Amount float64 `json:"amount,string"`
Date types.Time `json:"date"`
TradeID int64 `json:"tid,string"`
Price float64 `json:"price,string"`
Type int `json:"type,string"`
Amount float64 `json:"amount,string"`
}
// EURUSDConversionRate holds buy sell conversion rate information
@@ -101,49 +101,49 @@ type Balances map[string]Balance
// UserTransactions holds user transaction information
type UserTransactions struct {
Date string `json:"datetime"`
TransactionID int64 `json:"id"`
Type int64 `json:"type,string"`
USD float64 `json:"usd"`
EUR float64 `json:"eur"`
BTC float64 `json:"btc"`
XRP float64 `json:"xrp"`
BTCUSD float64 `json:"btc_usd"`
Fee float64 `json:"fee,string"`
OrderID int64 `json:"order_id"`
Date types.DateTime `json:"datetime"`
TransactionID int64 `json:"id"`
Type int64 `json:"type,string"`
USD types.Number `json:"usd"`
EUR types.Number `json:"eur"`
BTC types.Number `json:"btc"`
XRP types.Number `json:"xrp"`
BTCUSD types.Number `json:"btc_usd"`
Fee types.Number `json:"fee"`
OrderID int64 `json:"order_id"`
}
// Order holds current open order data
type Order struct {
ID int64 `json:"id,string"`
DateTime string `json:"datetime"`
Type int `json:"type,string"`
Price float64 `json:"price,string"`
Amount float64 `json:"amount,string"`
AmountAtCreate float64 `json:"amount_at_create,string"`
Currency string `json:"currency_pair"`
LimitPrice float64 `json:"limit_price,string"`
ClientOrderID string `json:"client_order_id"`
Market string `json:"market"`
ID int64 `json:"id,string"`
DateTime types.DateTime `json:"datetime"`
Type int64 `json:"type,string"`
Price float64 `json:"price,string"`
Amount float64 `json:"amount,string"`
AmountAtCreate float64 `json:"amount_at_create,string"`
Currency string `json:"currency_pair"`
LimitPrice float64 `json:"limit_price,string"`
ClientOrderID string `json:"client_order_id"`
Market string `json:"market"`
}
// OrderStatus holds order status information
type OrderStatus struct {
AmountRemaining float64 `json:"amount_remaining,string"`
Type int64 `json:"type"`
ID string `json:"id"`
DateTime string `json:"datetime"`
Status string `json:"status"`
ClientOrderID string `json:"client_order_id"`
Market string `json:"market"`
AmountRemaining float64 `json:"amount_remaining,string"`
Type int64 `json:"type"`
ID string `json:"id"`
DateTime types.DateTime `json:"datetime"`
Status string `json:"status"`
ClientOrderID string `json:"client_order_id"`
Market string `json:"market"`
Transactions []struct {
TradeID int64 `json:"tid"`
FromCurrency float64 `json:"{from_currency},string"`
ToCurrency float64 `json:"{to_currency},string"`
Price float64 `json:"price,string"`
Fee float64 `json:"fee,string"`
DateTime string `json:"datetime"`
Type int `json:"type"`
TradeID int64 `json:"tid"`
FromCurrency float64 `json:"{from_currency},string"`
ToCurrency float64 `json:"{to_currency},string"`
Price float64 `json:"price,string"`
Fee float64 `json:"fee,string"`
DateTime types.DateTime `json:"datetime"`
Type int64 `json:"type"`
}
}
@@ -163,16 +163,16 @@ type DepositAddress struct {
// WithdrawalRequests holds request information on withdrawals
type WithdrawalRequests struct {
OrderID int64 `json:"id"`
Date string `json:"datetime"`
Type int64 `json:"type"`
Amount float64 `json:"amount,string"`
Status int64 `json:"status"`
Currency currency.Code `json:"currency"`
Address string `json:"address"`
TransactionID string `json:"transaction_id"`
Network string `json:"network"`
TxID int64 `json:"txid"`
OrderID int64 `json:"id"`
Date types.DateTime `json:"datetime"`
Type int64 `json:"type"`
Amount float64 `json:"amount,string"`
Status int64 `json:"status"`
Currency currency.Code `json:"currency"`
Address string `json:"address"`
TransactionID string `json:"transaction_id"`
Network string `json:"network"`
TxID int64 `json:"txid"`
}
// CryptoWithdrawalResponse response from a crypto withdrawal request
@@ -228,16 +228,16 @@ type websocketTradeResponse struct {
}
type websocketTradeData struct {
Microtimestamp string `json:"microtimestamp"`
Amount float64 `json:"amount"`
BuyOrderID int64 `json:"buy_order_id"`
SellOrderID int64 `json:"sell_order_id"`
AmountStr string `json:"amount_str"`
PriceStr string `json:"price_str"`
Timestamp int64 `json:"timestamp,string"`
Price float64 `json:"price"`
Type int `json:"type"`
ID int64 `json:"id"`
Microtimestamp string `json:"microtimestamp"`
Amount float64 `json:"amount"`
BuyOrderID int64 `json:"buy_order_id"`
SellOrderID int64 `json:"sell_order_id"`
AmountStr string `json:"amount_str"`
PriceStr string `json:"price_str"`
Timestamp types.Time `json:"timestamp"`
Price float64 `json:"price"`
Type int `json:"type"`
ID int64 `json:"id"`
}
// WebsocketAuthResponse holds the auth token for subscribing to auth channels

View File

@@ -158,7 +158,7 @@ func (b *Bitstamp) handleWSTrade(msg []byte) error {
side = order.Sell
}
return trade.AddTradesToBuffer(trade.Data{
Timestamp: time.Unix(wsTradeTemp.Data.Timestamp, 0),
Timestamp: wsTradeTemp.Data.Timestamp.Time(),
CurrencyPair: p,
AssetType: asset.Spot,
Exchange: b.Name,

View File

@@ -264,7 +264,7 @@ func (b *Bitstamp) UpdateTicker(ctx context.Context, p currency.Pair, a asset.It
Volume: tick.Volume,
Open: tick.Open,
Pair: fPair,
LastUpdated: time.Unix(tick.Timestamp, 0),
LastUpdated: tick.Timestamp.Time(),
ExchangeName: b.Name,
AssetType: a,
})
@@ -386,15 +386,10 @@ func (b *Bitstamp) GetWithdrawalsHistory(ctx context.Context, c currency.Code, _
}
resp := make([]exchange.WithdrawalHistory, 0, len(withdrawals))
for i := range withdrawals {
var tm time.Time
tm, err = parseTime(withdrawals[i].Date)
if err != nil {
return nil, fmt.Errorf("getWithdrawalsHistory unable to parse Date field: %w", err)
}
if c.IsEmpty() || c.Equal(withdrawals[i].Currency) {
resp = append(resp, exchange.WithdrawalHistory{
Status: strconv.FormatInt(withdrawals[i].Status, 10),
Timestamp: tm,
Timestamp: withdrawals[i].Date.Time(),
Currency: withdrawals[i].Currency.String(),
Amount: withdrawals[i].Amount,
TransferType: strconv.FormatInt(withdrawals[i].Type, 10),
@@ -432,7 +427,7 @@ func (b *Bitstamp) GetRecentTrades(ctx context.Context, p currency.Pair, assetTy
Side: s,
Price: tradeData[i].Price,
Amount: tradeData[i].Amount,
Timestamp: time.Unix(tradeData[i].Date, 0),
Timestamp: tradeData[i].Date.Time(),
}
}
@@ -531,10 +526,6 @@ func (b *Bitstamp) GetOrderInfo(ctx context.Context, orderID string, _ currency.
Amount: o.Transactions[i].ToCurrency,
}
}
orderDate, err := time.Parse(time.DateTime, o.DateTime)
if err != nil {
return nil, err
}
status, err := order.StringToOrderStatus(o.Status)
if err != nil {
return nil, err
@@ -542,7 +533,7 @@ func (b *Bitstamp) GetOrderInfo(ctx context.Context, orderID string, _ currency.
return &order.Detail{
RemainingAmount: o.AmountRemaining,
OrderID: o.ID,
Date: orderDate,
Date: o.DateTime.Time(),
Trades: th,
Status: status,
}, nil
@@ -677,13 +668,6 @@ func (b *Bitstamp) GetActiveOrders(ctx context.Context, req *order.MultiOrderReq
orderSide = order.Sell
}
var tm time.Time
tm, err = parseTime(resp[i].DateTime)
if err != nil {
log.Errorf(log.ExchangeSys,
"%s GetActiveOrders unable to parse time: %s\n", b.Name, err)
}
var p currency.Pair
if currPair == "all" {
// Currency pairs are returned as format "currency_pair": "BTC/USD"
@@ -702,7 +686,7 @@ func (b *Bitstamp) GetActiveOrders(ctx context.Context, req *order.MultiOrderReq
Price: resp[i].Price,
Type: order.Limit,
Side: orderSide,
Date: tm,
Date: resp[i].DateTime.Time(),
Pair: p,
Exchange: b.Name,
}
@@ -776,16 +760,9 @@ func (b *Bitstamp) GetOrderHistory(ctx context.Context, req *order.MultiOrderReq
format.Delimiter)
}
var tm time.Time
tm, err = parseTime(resp[i].Date)
if err != nil {
log.Errorf(log.ExchangeSys,
"%s GetOrderHistory unable to parse time: %s\n", b.Name, err)
}
orders = append(orders, order.Detail{
OrderID: strconv.FormatInt(resp[i].OrderID, 10),
Date: tm,
Date: resp[i].Date.Time(),
Exchange: b.Name,
Pair: currPair,
})