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

@@ -153,48 +153,7 @@ func (by *Bybit) GetKlines(ctx context.Context, category, symbol string, interva
if err != nil {
return nil, err
}
return processKlineResponse(resp.List)
}
func processKlineResponse(in [][]string) ([]KlineItem, error) {
klines := make([]KlineItem, len(in))
for x := range in {
if len(in[x]) < 5 {
return nil, errors.New("invalid kline data")
}
startTimestamp, err := strconv.ParseInt(in[x][0], 10, 64)
if err != nil {
return nil, err
}
klines[x] = KlineItem{StartTime: time.UnixMilli(startTimestamp)}
klines[x].Open, err = strconv.ParseFloat(in[x][1], 64)
if err != nil {
return nil, err
}
klines[x].High, err = strconv.ParseFloat(in[x][2], 64)
if err != nil {
return nil, err
}
klines[x].Low, err = strconv.ParseFloat(in[x][3], 64)
if err != nil {
return nil, err
}
klines[x].Close, err = strconv.ParseFloat(in[x][4], 64)
if err != nil {
return nil, err
}
if len(in[x]) == 7 {
klines[x].TradeVolume, err = strconv.ParseFloat(in[x][5], 64)
if err != nil {
return nil, err
}
klines[x].Turnover, err = strconv.ParseFloat(in[x][6], 64)
if err != nil {
return nil, err
}
}
}
return klines, nil
return resp.List, nil
}
// GetInstrumentInfo retrieves the list of instrument details given the category and symbol.
@@ -244,7 +203,7 @@ func (by *Bybit) GetMarkPriceKline(ctx context.Context, category, symbol string,
if err != nil {
return nil, err
}
return processKlineResponse(resp.List)
return resp.List, nil
}
// GetIndexPriceKline query for historical index price klines. Charts are returned in groups based on the requested interval.
@@ -272,7 +231,7 @@ func (by *Bybit) GetIndexPriceKline(ctx context.Context, category, symbol string
if err != nil {
return nil, err
}
return processKlineResponse(resp.List)
return resp.List, nil
}
// GetOrderBook retrieves for orderbook depth data.

View File

@@ -75,25 +75,46 @@ func TestGetKlines(t *testing.T) {
s = time.Unix(1691897100, 0).Round(kline.FiveMin.Duration())
e = time.Unix(1691907100, 0).Round(kline.FiveMin.Duration())
}
_, err := b.GetKlines(t.Context(), "spot", spotTradablePair.String(), kline.FiveMin, s, e, 100)
if err != nil {
t.Fatal(err)
}
_, err = b.GetKlines(t.Context(), "linear", usdtMarginedTradablePair.String(), kline.FiveMin, s, e, 5)
if err != nil {
t.Fatal(err)
}
_, err = b.GetKlines(t.Context(), "linear", usdcMarginedTradablePair.String(), kline.FiveMin, s, e, 5)
if err != nil {
t.Fatal(err)
}
_, err = b.GetKlines(t.Context(), "inverse", inverseTradablePair.String(), kline.FiveMin, s, e, 5)
if err != nil {
t.Fatal(err)
}
_, err = b.GetKlines(t.Context(), "option", optionsTradablePair.String(), kline.FiveMin, s, e, 5)
if err == nil {
t.Fatalf("expected 'params error: Category is invalid', but found nil")
for _, tc := range []struct {
category string
pair currency.Pair
reqLimit uint64
expRespLen int
expError error
}{
{"spot", spotTradablePair, 100, 34, nil}, // TODO: Update expected limit when mock data is updated
{"linear", usdtMarginedTradablePair, 5, 5, nil},
{"linear", usdcMarginedTradablePair, 5, 5, nil},
{"inverse", inverseTradablePair, 5, 5, nil},
{"option", optionsTradablePair, 5, 5, errInvalidCategory},
} {
t.Run(fmt.Sprintf("%s-%s", tc.category, tc.pair), func(t *testing.T) {
t.Parallel()
r, err := b.GetKlines(t.Context(), tc.category, tc.pair.String(), kline.FiveMin, s, e, tc.reqLimit)
if tc.expError != nil {
require.ErrorIs(t, err, tc.expError)
return
}
require.NoError(t, err)
if mockTests {
require.Equal(t, tc.expRespLen, len(r))
switch tc.category {
case "spot":
assert.Equal(t, KlineItem{StartTime: types.Time(e), Open: 29393.99, High: 29399.76, Low: 29393.98, Close: 29399.76, TradeVolume: 1.168988, Turnover: 34363.5346739}, r[0])
case "linear":
if tc.pair == usdtMarginedTradablePair {
assert.Equal(t, KlineItem{StartTime: types.Time(e), Open: 0.0003, High: 0.0003, Low: 0.0002995, Close: 0.0003, TradeVolume: 55102100, Turnover: 16506.2427}, r[0])
return
}
assert.Equal(t, KlineItem{StartTime: types.Time(e), Open: 239.7, High: 239.7, Low: 239.7, Close: 239.7}, r[0])
case "inverse":
assert.Equal(t, KlineItem{StartTime: types.Time(e), Open: 0.2908, High: 0.2912, Low: 0.2908, Close: 0.2912, TradeVolume: 5131, Turnover: 17626.40000346}, r[0])
}
} else {
assert.NotEmpty(t, r)
}
})
}
}

View File

@@ -116,29 +116,34 @@ type RestResponse struct {
// KlineResponse represents a kline item list instance as an array of string.
type KlineResponse struct {
Symbol string `json:"symbol"`
Category string `json:"category"`
List [][]string `json:"list"`
Symbol string `json:"symbol"`
Category string `json:"category"`
List []KlineItem `json:"list"`
}
// KlineItem stores an individual kline data item
type KlineItem struct {
StartTime time.Time
Open float64
High float64
Low float64
Close float64
StartTime types.Time
Open types.Number
High types.Number
Low types.Number
Close types.Number
// not available for mark and index price kline data
TradeVolume float64
Turnover float64
TradeVolume types.Number
Turnover types.Number
}
// UnmarshalJSON implements the json.Unmarshaler interface for KlineItem
func (k *KlineItem) UnmarshalJSON(data []byte) error {
return json.Unmarshal(data, &[7]any{&k.StartTime, &k.Open, &k.High, &k.Low, &k.Close, &k.TradeVolume, &k.Turnover})
}
// MarkPriceKlineResponse represents a kline data item.
type MarkPriceKlineResponse struct {
Symbol string `json:"symbol"`
Category string `json:"category"`
List [][]string `json:"list"`
Symbol string `json:"symbol"`
Category string `json:"category"`
List []KlineItem `json:"list"`
}
func constructOrderbook(o *orderbookResponse) (*Orderbook, error) {

View File

@@ -1420,12 +1420,12 @@ func (by *Bybit) GetHistoricCandles(ctx context.Context, pair currency.Pair, a a
timeSeries = make([]kline.Candle, len(candles))
for x := range candles {
timeSeries[x] = kline.Candle{
Time: candles[x].StartTime,
Open: candles[x].Open,
High: candles[x].High,
Low: candles[x].Low,
Close: candles[x].Close,
Volume: candles[x].TradeVolume,
Time: candles[x].StartTime.Time(),
Open: candles[x].Open.Float64(),
High: candles[x].High.Float64(),
Low: candles[x].Low.Float64(),
Close: candles[x].Close.Float64(),
Volume: candles[x].TradeVolume.Float64(),
}
}
return req.ProcessResponse(timeSeries)
@@ -1461,12 +1461,12 @@ func (by *Bybit) GetHistoricCandlesExtended(ctx context.Context, pair currency.P
for i := range klineItems {
timeSeries = append(timeSeries, kline.Candle{
Time: klineItems[i].StartTime,
Open: klineItems[i].Open,
High: klineItems[i].High,
Low: klineItems[i].Low,
Close: klineItems[i].Close,
Volume: klineItems[i].TradeVolume,
Time: klineItems[i].StartTime.Time(),
Open: klineItems[i].Open.Float64(),
High: klineItems[i].High.Float64(),
Low: klineItems[i].Low.Float64(),
Close: klineItems[i].Close.Float64(),
Volume: klineItems[i].TradeVolume.Float64(),
})
}
}