mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-26 00:35:15 +00:00
pkg/exchage: support k line rest api
This commit is contained in:
parent
24d240b1f3
commit
e9d0ce5bbf
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
107
pkg/exchange/bybit/bybitapi/get_k_lines_request.go
Normal file
107
pkg/exchange/bybit/bybitapi/get_k_lines_request.go
Normal 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,
|
||||
}
|
||||
}
|
237
pkg/exchange/bybit/bybitapi/get_k_lines_request_requestgen.go
Normal file
237
pkg/exchange/bybit/bybitapi/get_k_lines_request_requestgen.go
Normal 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
|
||||
}
|
175
pkg/exchange/bybit/bybitapi/get_k_lines_request_test.go
Normal file
175
pkg/exchange/bybit/bybitapi/get_k_lines_request_test.go
Normal 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], ×tamp)
|
||||
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)
|
||||
})
|
||||
}
|
|
@ -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 (
|
||||
|
|
41
pkg/exchange/bybit/bybitapi/types_test.go
Normal file
41
pkg/exchange/bybit/bybitapi/types_test.go
Normal 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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user