pkg/exchage: support k line rest api

This commit is contained in:
Edwin 2023-08-09 11:41:04 +08:00
parent 24d240b1f3
commit e9d0ce5bbf
9 changed files with 806 additions and 1 deletions

View File

@ -5,6 +5,7 @@ import (
"os"
"strconv"
"testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
@ -162,4 +163,14 @@ func TestClient(t *testing.T) {
assert.NoError(t, err)
t.Logf("apiResp: %+v", apiResp)
})
t.Run("GetKLinesRequest", func(t *testing.T) {
startTime := time.Date(2023, 8, 8, 9, 28, 0, 0, time.UTC)
endTime := time.Date(2023, 8, 8, 9, 45, 0, 0, time.UTC)
req := client.NewGetKLinesRequest().
Symbol("BTCUSDT").Interval("15").StartTime(startTime).EndTime(endTime)
apiResp, err := req.Do(ctx)
assert.NoError(t, err)
t.Logf("apiResp: %+v", apiResp.List)
})
}

View File

@ -0,0 +1,107 @@
package bybitapi
import (
"encoding/json"
"fmt"
"time"
"github.com/c9s/requestgen"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
)
//go:generate -command GetRequest requestgen -method GET -responseType .APIResponse -responseDataField Result
//go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Result
type IntervalSign string
const (
IntervalSignDay IntervalSign = "D"
IntervalSignWeek IntervalSign = "W"
IntervalSignMonth IntervalSign = "M"
)
type KLinesResponse struct {
Symbol string `json:"symbol"`
// An string array of individual candle
// Sort in reverse by startTime
List []KLine `json:"list"`
Category Category `json:"category"`
}
type KLine struct {
// list[0]: startTime, Start time of the candle (ms)
StartTime types.MillisecondTimestamp
// list[1]: openPrice
Open fixedpoint.Value
// list[2]: highPrice
High fixedpoint.Value
// list[3]: lowPrice
Low fixedpoint.Value
// list[4]: closePrice
Close fixedpoint.Value
// list[5]: volume, Trade volume. Unit of contract: pieces of contract. Unit of spot: quantity of coins
Volume fixedpoint.Value
// list[6]: turnover, Turnover. Unit of figure: quantity of quota coin
TurnOver fixedpoint.Value
}
const KLinesArrayLen = 7
func (k *KLine) UnmarshalJSON(data []byte) error {
var jsonArr []json.RawMessage
err := json.Unmarshal(data, &jsonArr)
if err != nil {
return fmt.Errorf("failed to unmarshal jsonRawMessage: %v, err: %w", string(data), err)
}
if len(jsonArr) != KLinesArrayLen {
return fmt.Errorf("unexpected K Lines array length: %d, exp: %d", len(jsonArr), KLinesArrayLen)
}
err = json.Unmarshal(jsonArr[0], &k.StartTime)
if err != nil {
return fmt.Errorf("failed to unmarshal resp index 0: %v, err: %w", string(jsonArr[0]), err)
}
values := make([]fixedpoint.Value, len(jsonArr)-1)
for i, jsonRaw := range jsonArr[1:] {
err = json.Unmarshal(jsonRaw, &values[i])
if err != nil {
return fmt.Errorf("failed to unmarshal resp index %d: %v, err: %w", i+1, string(jsonRaw), err)
}
}
k.Open = values[0]
k.High = values[1]
k.Low = values[2]
k.Close = values[3]
k.Volume = values[4]
k.TurnOver = values[5]
return nil
}
//go:generate GetRequest -url "/v5/market/kline" -type GetKLinesRequest -responseDataType .KLinesResponse
type GetKLinesRequest struct {
client requestgen.APIClient
category Category `param:"category,query" validValues:"spot"`
symbol string `param:"symbol,query"`
// Kline interval.
// - 1,3,5,15,30,60,120,240,360,720: minute
// - D: day
// - M: month
// - W: week
interval string `param:"interval,query" validValues:"1,3,5,15,30,60,120,240,360,720,D,W,M"`
startTime *time.Time `param:"start,query,milliseconds"`
endTime *time.Time `param:"end,query,milliseconds"`
// Limit for data size per page. [1, 1000]. Default: 200
limit *uint64 `param:"limit,query"`
}
func (c *RestClient) NewGetKLinesRequest() *GetKLinesRequest {
return &GetKLinesRequest{
client: c,
category: CategorySpot,
}
}

View File

@ -0,0 +1,237 @@
// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Result -url /v5/market/kline -type GetKLinesRequest -responseDataType .KLinesResponse"; DO NOT EDIT.
package bybitapi
import (
"context"
"encoding/json"
"fmt"
"net/url"
"reflect"
"regexp"
"strconv"
"time"
)
func (g *GetKLinesRequest) Category(category Category) *GetKLinesRequest {
g.category = category
return g
}
func (g *GetKLinesRequest) Symbol(symbol string) *GetKLinesRequest {
g.symbol = symbol
return g
}
func (g *GetKLinesRequest) Interval(interval string) *GetKLinesRequest {
g.interval = interval
return g
}
func (g *GetKLinesRequest) StartTime(startTime time.Time) *GetKLinesRequest {
g.startTime = &startTime
return g
}
func (g *GetKLinesRequest) EndTime(endTime time.Time) *GetKLinesRequest {
g.endTime = &endTime
return g
}
func (g *GetKLinesRequest) Limit(limit uint64) *GetKLinesRequest {
g.limit = &limit
return g
}
// GetQueryParameters builds and checks the query parameters and returns url.Values
func (g *GetKLinesRequest) GetQueryParameters() (url.Values, error) {
var params = map[string]interface{}{}
// check category field -> json key category
category := g.category
// TEMPLATE check-valid-values
switch category {
case "spot":
params["category"] = category
default:
return nil, fmt.Errorf("category value %v is invalid", category)
}
// END TEMPLATE check-valid-values
// assign parameter of category
params["category"] = category
// check symbol field -> json key symbol
symbol := g.symbol
// assign parameter of symbol
params["symbol"] = symbol
// check interval field -> json key interval
interval := g.interval
// TEMPLATE check-valid-values
switch interval {
case "1", "3", "5", "15", "30", "60", "120", "240", "360", "720", "D", "W", "M":
params["interval"] = interval
default:
return nil, fmt.Errorf("interval value %v is invalid", interval)
}
// END TEMPLATE check-valid-values
// assign parameter of interval
params["interval"] = interval
// check startTime field -> json key start
if g.startTime != nil {
startTime := *g.startTime
// assign parameter of startTime
// convert time.Time to milliseconds time stamp
params["start"] = strconv.FormatInt(startTime.UnixNano()/int64(time.Millisecond), 10)
} else {
}
// check endTime field -> json key end
if g.endTime != nil {
endTime := *g.endTime
// assign parameter of endTime
// convert time.Time to milliseconds time stamp
params["end"] = strconv.FormatInt(endTime.UnixNano()/int64(time.Millisecond), 10)
} else {
}
// check limit field -> json key limit
if g.limit != nil {
limit := *g.limit
// assign parameter of limit
params["limit"] = limit
} else {
}
query := url.Values{}
for _k, _v := range params {
query.Add(_k, fmt.Sprintf("%v", _v))
}
return query, nil
}
// GetParameters builds and checks the parameters and return the result in a map object
func (g *GetKLinesRequest) GetParameters() (map[string]interface{}, error) {
var params = map[string]interface{}{}
return params, nil
}
// GetParametersQuery converts the parameters from GetParameters into the url.Values format
func (g *GetKLinesRequest) GetParametersQuery() (url.Values, error) {
query := url.Values{}
params, err := g.GetParameters()
if err != nil {
return query, err
}
for _k, _v := range params {
if g.isVarSlice(_v) {
g.iterateSlice(_v, func(it interface{}) {
query.Add(_k+"[]", fmt.Sprintf("%v", it))
})
} else {
query.Add(_k, fmt.Sprintf("%v", _v))
}
}
return query, nil
}
// GetParametersJSON converts the parameters from GetParameters into the JSON format
func (g *GetKLinesRequest) GetParametersJSON() ([]byte, error) {
params, err := g.GetParameters()
if err != nil {
return nil, err
}
return json.Marshal(params)
}
// GetSlugParameters builds and checks the slug parameters and return the result in a map object
func (g *GetKLinesRequest) GetSlugParameters() (map[string]interface{}, error) {
var params = map[string]interface{}{}
return params, nil
}
func (g *GetKLinesRequest) applySlugsToUrl(url string, slugs map[string]string) string {
for _k, _v := range slugs {
needleRE := regexp.MustCompile(":" + _k + "\\b")
url = needleRE.ReplaceAllString(url, _v)
}
return url
}
func (g *GetKLinesRequest) iterateSlice(slice interface{}, _f func(it interface{})) {
sliceValue := reflect.ValueOf(slice)
for _i := 0; _i < sliceValue.Len(); _i++ {
it := sliceValue.Index(_i).Interface()
_f(it)
}
}
func (g *GetKLinesRequest) isVarSlice(_v interface{}) bool {
rt := reflect.TypeOf(_v)
switch rt.Kind() {
case reflect.Slice:
return true
}
return false
}
func (g *GetKLinesRequest) GetSlugsMap() (map[string]string, error) {
slugs := map[string]string{}
params, err := g.GetSlugParameters()
if err != nil {
return slugs, nil
}
for _k, _v := range params {
slugs[_k] = fmt.Sprintf("%v", _v)
}
return slugs, nil
}
func (g *GetKLinesRequest) Do(ctx context.Context) (*KLinesResponse, error) {
// no body params
var params interface{}
query, err := g.GetQueryParameters()
if err != nil {
return nil, err
}
apiURL := "/v5/market/kline"
req, err := g.client.NewRequest(ctx, "GET", apiURL, query, params)
if err != nil {
return nil, err
}
response, err := g.client.SendRequest(req)
if err != nil {
return nil, err
}
var apiResponse APIResponse
if err := response.DecodeJSON(&apiResponse); err != nil {
return nil, err
}
var data KLinesResponse
if err := json.Unmarshal(apiResponse.Result, &data); err != nil {
return nil, err
}
return &data, nil
}

View File

@ -0,0 +1,175 @@
package bybitapi
import (
"encoding/json"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
)
func TestKLinesResponse_UnmarshalJSON(t *testing.T) {
t.Run("succeeds", func(t *testing.T) {
data := `{
"symbol": "BTCUSDT",
"category": "spot",
"list": [
[
"1670608800000",
"17071",
"17073",
"17027",
"17055.5",
"268611",
"15.74462667"
],
[
"1670605200000",
"17071.5",
"17071.5",
"17061",
"17071",
"4177",
"0.24469757"
]
]
}`
expRes := &KLinesResponse{
Symbol: "BTCUSDT",
List: []KLine{
{
StartTime: types.NewMillisecondTimestampFromInt(1670608800000),
Open: fixedpoint.NewFromFloat(17071),
High: fixedpoint.NewFromFloat(17073),
Low: fixedpoint.NewFromFloat(17027),
Close: fixedpoint.NewFromFloat(17055.5),
Volume: fixedpoint.NewFromFloat(268611),
TurnOver: fixedpoint.NewFromFloat(15.74462667),
},
{
StartTime: types.NewMillisecondTimestampFromInt(1670605200000),
Open: fixedpoint.NewFromFloat(17071.5),
High: fixedpoint.NewFromFloat(17071.5),
Low: fixedpoint.NewFromFloat(17061),
Close: fixedpoint.NewFromFloat(17071),
Volume: fixedpoint.NewFromFloat(4177),
TurnOver: fixedpoint.NewFromFloat(0.24469757),
},
},
Category: CategorySpot,
}
kline := &KLinesResponse{}
err := json.Unmarshal([]byte(data), kline)
assert.NoError(t, err)
assert.Equal(t, expRes, kline)
})
t.Run("unexpected length", func(t *testing.T) {
data := `{
"symbol": "BTCUSDT",
"category": "spot",
"list": [
[
"1670608800000",
"17071",
"17073",
"17027",
"17055.5",
"268611"
]
]
}`
kline := &KLinesResponse{}
err := json.Unmarshal([]byte(data), kline)
assert.Equal(t, fmt.Errorf("unexpected K Lines array length: 6, exp: %d", KLinesArrayLen), err)
})
t.Run("unexpected json array", func(t *testing.T) {
klineJson := `{}`
data := fmt.Sprintf(`{
"symbol": "BTCUSDT",
"category": "spot",
"list": [%s]
}`, klineJson)
var jsonArr []json.RawMessage
expErr := json.Unmarshal([]byte(klineJson), &jsonArr)
assert.Error(t, expErr)
kline := &KLinesResponse{}
err := json.Unmarshal([]byte(data), kline)
assert.Equal(t, fmt.Errorf("failed to unmarshal jsonRawMessage: %v, err: %w", klineJson, expErr), err)
})
t.Run("unexpected json 0", func(t *testing.T) {
klineJson := `
[
"a",
"17071.5",
"17071.5",
"17061",
"17071",
"4177",
"0.24469757"
]
`
data := fmt.Sprintf(`{
"symbol": "BTCUSDT",
"category": "spot",
"list": [%s]
}`, klineJson)
var jsonArr []json.RawMessage
err := json.Unmarshal([]byte(klineJson), &jsonArr)
assert.NoError(t, err)
timestamp := types.MillisecondTimestamp{}
expErr := json.Unmarshal(jsonArr[0], &timestamp)
assert.NoError(t, err)
kline := &KLinesResponse{}
err = json.Unmarshal([]byte(data), kline)
assert.Equal(t, fmt.Errorf("failed to unmarshal resp index 0: %v, err: %w", string(jsonArr[0]), expErr), err)
})
t.Run("unexpected json 1", func(t *testing.T) {
// TODO: fix panic
t.Skip("test will result in a panic, skip it")
klineJson := `
[
"1670608800000",
"a",
"17071.5",
"17061",
"17071",
"4177",
"0.24469757"
]
`
data := fmt.Sprintf(`{
"symbol": "BTCUSDT",
"category": "spot",
"list": [%s]
}`, klineJson)
var jsonArr []json.RawMessage
err := json.Unmarshal([]byte(klineJson), &jsonArr)
assert.NoError(t, err)
var value fixedpoint.Value
expErr := json.Unmarshal(jsonArr[1], &value)
assert.NoError(t, err)
kline := &KLinesResponse{}
err = json.Unmarshal([]byte(data), kline)
assert.Equal(t, fmt.Errorf("failed to unmarshal resp index 1: %v, err: %w", string(jsonArr[1]), expErr), err)
})
}

View File

@ -1,5 +1,41 @@
package bybitapi
import "github.com/c9s/bbgo/pkg/types"
var (
SupportedIntervals = map[types.Interval]int{
types.Interval1m: 1 * 60,
types.Interval3m: 3 * 60,
types.Interval5m: 5 * 60,
types.Interval15m: 15 * 60,
types.Interval30m: 30 * 60,
types.Interval1h: 60 * 60,
types.Interval2h: 60 * 60 * 2,
types.Interval4h: 60 * 60 * 4,
types.Interval6h: 60 * 60 * 6,
types.Interval12h: 60 * 60 * 12,
types.Interval1d: 60 * 60 * 24,
types.Interval1w: 60 * 60 * 24 * 7,
types.Interval1mo: 60 * 60 * 24 * 30,
}
ToGlobalInterval = map[string]types.Interval{
"1": types.Interval1m,
"3": types.Interval3m,
"5": types.Interval5m,
"15": types.Interval15m,
"30": types.Interval30m,
"60": types.Interval1h,
"120": types.Interval2h,
"240": types.Interval4h,
"360": types.Interval6h,
"720": types.Interval12h,
"D": types.Interval1d,
"W": types.Interval1w,
"M": types.Interval1mo,
}
)
type Category string
const (

View File

@ -0,0 +1,41 @@
package bybitapi
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/c9s/bbgo/pkg/types"
)
func Test_SupportedIntervals(t *testing.T) {
assert.Equal(t, SupportedIntervals[types.Interval1m], 60)
assert.Equal(t, SupportedIntervals[types.Interval3m], 180)
assert.Equal(t, SupportedIntervals[types.Interval5m], 300)
assert.Equal(t, SupportedIntervals[types.Interval15m], 15*60)
assert.Equal(t, SupportedIntervals[types.Interval30m], 30*60)
assert.Equal(t, SupportedIntervals[types.Interval1h], 60*60)
assert.Equal(t, SupportedIntervals[types.Interval2h], 60*60*2)
assert.Equal(t, SupportedIntervals[types.Interval4h], 60*60*4)
assert.Equal(t, SupportedIntervals[types.Interval6h], 60*60*6)
assert.Equal(t, SupportedIntervals[types.Interval12h], 60*60*12)
assert.Equal(t, SupportedIntervals[types.Interval1d], 60*60*24)
assert.Equal(t, SupportedIntervals[types.Interval1w], 60*60*24*7)
assert.Equal(t, SupportedIntervals[types.Interval1mo], 60*60*24*30)
}
func Test_ToGlobalInterval(t *testing.T) {
assert.Equal(t, ToGlobalInterval["1"], types.Interval1m)
assert.Equal(t, ToGlobalInterval["3"], types.Interval3m)
assert.Equal(t, ToGlobalInterval["5"], types.Interval5m)
assert.Equal(t, ToGlobalInterval["15"], types.Interval15m)
assert.Equal(t, ToGlobalInterval["30"], types.Interval30m)
assert.Equal(t, ToGlobalInterval["60"], types.Interval1h)
assert.Equal(t, ToGlobalInterval["120"], types.Interval2h)
assert.Equal(t, ToGlobalInterval["240"], types.Interval4h)
assert.Equal(t, ToGlobalInterval["360"], types.Interval6h)
assert.Equal(t, ToGlobalInterval["720"], types.Interval12h)
assert.Equal(t, ToGlobalInterval["D"], types.Interval1d)
assert.Equal(t, ToGlobalInterval["W"], types.Interval1w)
assert.Equal(t, ToGlobalInterval["M"], types.Interval1mo)
}

View File

@ -283,3 +283,48 @@ func toGlobalBalanceMap(events []bybitapi.WalletBalances) types.BalanceMap {
}
return bm
}
func toLocalInterval(interval types.Interval) (string, error) {
if _, found := bybitapi.SupportedIntervals[interval]; !found {
return "", fmt.Errorf("interval not supported: %s", interval)
}
switch interval {
case types.Interval1d:
return string(bybitapi.IntervalSignDay), nil
case types.Interval1w:
return string(bybitapi.IntervalSignWeek), nil
case types.Interval1mo:
return string(bybitapi.IntervalSignMonth), nil
default:
return fmt.Sprintf("%d", interval.Minutes()), nil
}
}
func toGlobalKLines(symbol string, interval types.Interval, klines []bybitapi.KLine) []types.KLine {
gKLines := make([]types.KLine, len(klines))
for i, kline := range klines {
endTime := types.Time(kline.StartTime.Time().Add(interval.Duration()))
gKLines[i] = types.KLine{
Exchange: types.ExchangeBybit,
Symbol: symbol,
StartTime: types.Time(kline.StartTime),
EndTime: endTime,
Interval: interval,
Open: kline.Open,
Close: kline.Close,
High: kline.High,
Low: kline.Low,
Volume: kline.Volume,
QuoteVolume: kline.TurnOver,
// Bybit doesn't support close flag in REST API
Closed: false,
}
}
return gKLines
}

View File

@ -459,3 +459,88 @@ func Test_toGlobalTrade(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, res, &exp)
}
func Test_toGlobalKLines(t *testing.T) {
symbol := "BTCUSDT"
interval := types.Interval15m
resp := bybitapi.KLinesResponse{
Symbol: symbol,
List: []bybitapi.KLine{
/*
[
{
"StartTime": "2023-08-08 17:30:00 +0800 CST",
"OpenPrice": 29045.3,
"HighPrice": 29228.56,
"LowPrice": 29045.3,
"ClosePrice": 29228.56,
"Volume": 9.265593,
"TurnOver": 270447.43520753
},
{
"StartTime": "2023-08-08 17:15:00 +0800 CST",
"OpenPrice": 29167.33,
"HighPrice": 29229.08,
"LowPrice": 29000,
"ClosePrice": 29045.3,
"Volume": 9.295508,
"TurnOver": 270816.87513775
}
]
*/
{
StartTime: types.NewMillisecondTimestampFromInt(1691486100000),
Open: fixedpoint.NewFromFloat(29045.3),
High: fixedpoint.NewFromFloat(29228.56),
Low: fixedpoint.NewFromFloat(29045.3),
Close: fixedpoint.NewFromFloat(29228.56),
Volume: fixedpoint.NewFromFloat(9.265593),
TurnOver: fixedpoint.NewFromFloat(270447.43520753),
},
{
StartTime: types.NewMillisecondTimestampFromInt(1691487000000),
Open: fixedpoint.NewFromFloat(29167.33),
High: fixedpoint.NewFromFloat(29229.08),
Low: fixedpoint.NewFromFloat(29000),
Close: fixedpoint.NewFromFloat(29045.3),
Volume: fixedpoint.NewFromFloat(9.295508),
TurnOver: fixedpoint.NewFromFloat(270816.87513775),
},
},
Category: bybitapi.CategorySpot,
}
expKlines := []types.KLine{
{
Exchange: types.ExchangeBybit,
Symbol: resp.Symbol,
StartTime: types.Time(resp.List[0].StartTime.Time()),
EndTime: types.Time(resp.List[0].StartTime.Time().Add(interval.Duration())),
Interval: interval,
Open: fixedpoint.NewFromFloat(29045.3),
Close: fixedpoint.NewFromFloat(29228.56),
High: fixedpoint.NewFromFloat(29228.56),
Low: fixedpoint.NewFromFloat(29045.3),
Volume: fixedpoint.NewFromFloat(9.265593),
QuoteVolume: fixedpoint.NewFromFloat(270447.43520753),
Closed: false,
},
{
Exchange: types.ExchangeBybit,
Symbol: resp.Symbol,
StartTime: types.Time(resp.List[1].StartTime.Time()),
EndTime: types.Time(resp.List[1].StartTime.Time().Add(interval.Duration())),
Interval: interval,
Open: fixedpoint.NewFromFloat(29167.33),
Close: fixedpoint.NewFromFloat(29045.3),
High: fixedpoint.NewFromFloat(29229.08),
Low: fixedpoint.NewFromFloat(29000),
Volume: fixedpoint.NewFromFloat(9.295508),
QuoteVolume: fixedpoint.NewFromFloat(270816.87513775),
Closed: false,
},
}
assert.Equal(t, toGlobalKLines(symbol, interval, resp.List), expKlines)
}

View File

@ -19,6 +19,7 @@ import (
const (
maxOrderIdLen = 36
defaultQueryLimit = 50
defaultKLineLimit = 1000
halfYearDuration = 6 * 30 * 24 * time.Hour
)
@ -38,7 +39,12 @@ var (
"exchange": "bybit",
})
_ types.ExchangeAccountService = &Exchange{}
_ types.ExchangeAccountService = &Exchange{}
_ types.ExchangeMarketDataService = &Exchange{}
_ types.CustomIntervalProvider = &Exchange{}
_ types.ExchangeMinimal = &Exchange{}
_ types.ExchangeTradeService = &Exchange{}
_ types.Exchange = &Exchange{}
)
type Exchange struct {
@ -422,6 +428,68 @@ func (e *Exchange) QueryAccountBalances(ctx context.Context) (types.BalanceMap,
return toGlobalBalanceMap(accounts.List), nil
}
/*
QueryKLines queries for historical klines (also known as candles/candlesticks). Charts are returned in groups based
on the requested interval.
A k-line's start time is inclusive, but end time is not(startTime + interval - 1 millisecond).
e.q. 15m interval k line can be represented as 00:00:00.000 ~ 00:14:59.999
*/
func (e *Exchange) QueryKLines(ctx context.Context, symbol string, interval types.Interval, options types.KLineQueryOptions) ([]types.KLine, error) {
req := e.client.NewGetKLinesRequest().Symbol(symbol)
intervalStr, err := toLocalInterval(interval)
if err != nil {
return nil, err
}
req.Interval(intervalStr)
limit := uint64(options.Limit)
if limit > defaultKLineLimit || limit <= 0 {
log.Debugf("limtit is exceeded or zero, update to %d, got: %d", defaultKLineLimit, options.Limit)
limit = defaultKLineLimit
}
req.Limit(limit)
if options.StartTime != nil {
req.StartTime(*options.StartTime)
}
if options.EndTime != nil {
req.EndTime(*options.EndTime)
}
if err := sharedRateLimiter.Wait(ctx); err != nil {
return nil, fmt.Errorf("query klines rate limiter wait error: %w", err)
}
resp, err := req.Do(ctx)
if err != nil {
return nil, fmt.Errorf("failed to call k line, err: %w", err)
}
if resp.Category != bybitapi.CategorySpot {
return nil, fmt.Errorf("unexpected category: %s", resp.Category)
}
if resp.Symbol != symbol {
return nil, fmt.Errorf("unexpected symbol: %s, exp: %s", resp.Category, symbol)
}
kLines := toGlobalKLines(symbol, interval, resp.List)
return types.SortKLinesAscending(kLines), nil
}
func (e *Exchange) SupportedInterval() map[types.Interval]int {
return bybitapi.SupportedIntervals
}
func (e *Exchange) IsSupportedInterval(interval types.Interval) bool {
_, ok := bybitapi.SupportedIntervals[interval]
return ok
}
func (e *Exchange) NewStream() types.Stream {
return NewStream(e.key, e.secret)
}