mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-25 16:25:16 +00:00
Merge pull request #1410 from c9s/edwin/bitget/query-kline
FEATURE: [bitget] Add query kline
This commit is contained in:
commit
868d48b3b1
|
@ -5,6 +5,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
@ -78,4 +79,12 @@ func TestClient(t *testing.T) {
|
||||||
resp, err := client.NewCancelOrderRequest().Symbol("APEUSDT").OrderId(req.OrderId).Do(ctx)
|
resp, err := client.NewCancelOrderRequest().Symbol("APEUSDT").OrderId(req.OrderId).Do(ctx)
|
||||||
t.Logf("cancel order resp: %+v", resp)
|
t.Logf("cancel order resp: %+v", resp)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("GetKLineRequest", func(t *testing.T) {
|
||||||
|
startTime := time.Date(2023, 8, 12, 0, 0, 0, 0, time.UTC)
|
||||||
|
endTime := time.Date(2023, 10, 14, 0, 0, 0, 0, time.UTC)
|
||||||
|
resp, err := client.NewGetKLineRequest().Symbol("APEUSDT").Granularity("30min").StartTime(startTime).EndTime(endTime).Limit("1000").Do(ctx)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
t.Logf("resp: %+v", resp)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -188,6 +188,12 @@ func (g *GetHistoryOrdersRequest) GetSlugsMap() (map[string]string, error) {
|
||||||
return slugs, nil
|
return slugs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetPath returns the request path of the API
|
||||||
|
func (g *GetHistoryOrdersRequest) GetPath() string {
|
||||||
|
return "/api/v2/spot/trade/history-orders"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do generates the request object and send the request object to the API endpoint
|
||||||
func (g *GetHistoryOrdersRequest) Do(ctx context.Context) ([]OrderDetail, error) {
|
func (g *GetHistoryOrdersRequest) Do(ctx context.Context) ([]OrderDetail, error) {
|
||||||
|
|
||||||
// no body params
|
// no body params
|
||||||
|
@ -197,7 +203,9 @@ func (g *GetHistoryOrdersRequest) Do(ctx context.Context) ([]OrderDetail, error)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
apiURL := "/api/v2/spot/trade/history-orders"
|
var apiURL string
|
||||||
|
|
||||||
|
apiURL = g.GetPath()
|
||||||
|
|
||||||
req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params)
|
req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -214,6 +222,15 @@ func (g *GetHistoryOrdersRequest) Do(ctx context.Context) ([]OrderDetail, error)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type responseValidator interface {
|
||||||
|
Validate() error
|
||||||
|
}
|
||||||
|
validator, ok := interface{}(apiResponse).(responseValidator)
|
||||||
|
if ok {
|
||||||
|
if err := validator.Validate(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
var data []OrderDetail
|
var data []OrderDetail
|
||||||
if err := json.Unmarshal(apiResponse.Data, &data); err != nil {
|
if err := json.Unmarshal(apiResponse.Data, &data); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
83
pkg/exchange/bitget/bitgetapi/v2/get_k_line.go
Normal file
83
pkg/exchange/bitget/bitgetapi/v2/get_k_line.go
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
package bitgetapi
|
||||||
|
|
||||||
|
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 Data
|
||||||
|
//go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Data
|
||||||
|
|
||||||
|
type KLine struct {
|
||||||
|
// System timestamp, Unix millisecond timestamp, e.g. 1690196141868
|
||||||
|
Ts types.MillisecondTimestamp
|
||||||
|
Open fixedpoint.Value
|
||||||
|
High fixedpoint.Value
|
||||||
|
Low fixedpoint.Value
|
||||||
|
Close fixedpoint.Value
|
||||||
|
// Trading volume in base currency, e.g. "BTC" in the "BTCUSD" pair.
|
||||||
|
Volume fixedpoint.Value
|
||||||
|
// Trading volume in quote currency, e.g. "USD" in the "BTCUSD" pair.
|
||||||
|
QuoteVolume fixedpoint.Value
|
||||||
|
// Trading volume in USDT
|
||||||
|
UsdtVolume fixedpoint.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
type KLineResponse []KLine
|
||||||
|
|
||||||
|
const KLinesArrayLen = 8
|
||||||
|
|
||||||
|
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.Ts)
|
||||||
|
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.QuoteVolume = values[5]
|
||||||
|
k.UsdtVolume = values[6]
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//go:generate GetRequest -url "/api/v2/spot/market/candles" -type GetKLineRequest -responseDataType .KLineResponse
|
||||||
|
type GetKLineRequest struct {
|
||||||
|
client requestgen.APIClient
|
||||||
|
|
||||||
|
symbol string `param:"symbol,query"`
|
||||||
|
granularity string `param:"granularity,query"`
|
||||||
|
startTime *time.Time `param:"startTime,milliseconds,query"`
|
||||||
|
endTime *time.Time `param:"endTime,milliseconds,query"`
|
||||||
|
// Limit number default 100 max 1000
|
||||||
|
limit *string `param:"limit,query"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Client) NewGetKLineRequest() *GetKLineRequest {
|
||||||
|
return &GetKLineRequest{client: s.Client}
|
||||||
|
}
|
|
@ -0,0 +1,224 @@
|
||||||
|
// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Data -url /api/v2/spot/market/candles -type GetKLineRequest -responseDataType .KLineResponse"; DO NOT EDIT.
|
||||||
|
|
||||||
|
package bitgetapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/c9s/bbgo/pkg/exchange/bitget/bitgetapi"
|
||||||
|
"net/url"
|
||||||
|
"reflect"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (g *GetKLineRequest) Symbol(symbol string) *GetKLineRequest {
|
||||||
|
g.symbol = symbol
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GetKLineRequest) Granularity(granularity string) *GetKLineRequest {
|
||||||
|
g.granularity = granularity
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GetKLineRequest) StartTime(startTime time.Time) *GetKLineRequest {
|
||||||
|
g.startTime = &startTime
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GetKLineRequest) EndTime(endTime time.Time) *GetKLineRequest {
|
||||||
|
g.endTime = &endTime
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GetKLineRequest) Limit(limit string) *GetKLineRequest {
|
||||||
|
g.limit = &limit
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetQueryParameters builds and checks the query parameters and returns url.Values
|
||||||
|
func (g *GetKLineRequest) GetQueryParameters() (url.Values, error) {
|
||||||
|
var params = map[string]interface{}{}
|
||||||
|
// check symbol field -> json key symbol
|
||||||
|
symbol := g.symbol
|
||||||
|
|
||||||
|
// assign parameter of symbol
|
||||||
|
params["symbol"] = symbol
|
||||||
|
// check granularity field -> json key granularity
|
||||||
|
granularity := g.granularity
|
||||||
|
|
||||||
|
// assign parameter of granularity
|
||||||
|
params["granularity"] = granularity
|
||||||
|
// check startTime field -> json key startTime
|
||||||
|
if g.startTime != nil {
|
||||||
|
startTime := *g.startTime
|
||||||
|
|
||||||
|
// assign parameter of startTime
|
||||||
|
// convert time.Time to milliseconds time stamp
|
||||||
|
params["startTime"] = strconv.FormatInt(startTime.UnixNano()/int64(time.Millisecond), 10)
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
// check endTime field -> json key endTime
|
||||||
|
if g.endTime != nil {
|
||||||
|
endTime := *g.endTime
|
||||||
|
|
||||||
|
// assign parameter of endTime
|
||||||
|
// convert time.Time to milliseconds time stamp
|
||||||
|
params["endTime"] = 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 *GetKLineRequest) 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 *GetKLineRequest) 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 *GetKLineRequest) 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 *GetKLineRequest) GetSlugParameters() (map[string]interface{}, error) {
|
||||||
|
var params = map[string]interface{}{}
|
||||||
|
|
||||||
|
return params, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GetKLineRequest) 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 *GetKLineRequest) 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 *GetKLineRequest) isVarSlice(_v interface{}) bool {
|
||||||
|
rt := reflect.TypeOf(_v)
|
||||||
|
switch rt.Kind() {
|
||||||
|
case reflect.Slice:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GetKLineRequest) 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPath returns the request path of the API
|
||||||
|
func (g *GetKLineRequest) GetPath() string {
|
||||||
|
return "/api/v2/spot/market/candles"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do generates the request object and send the request object to the API endpoint
|
||||||
|
func (g *GetKLineRequest) Do(ctx context.Context) (KLineResponse, error) {
|
||||||
|
|
||||||
|
// no body params
|
||||||
|
var params interface{}
|
||||||
|
query, err := g.GetQueryParameters()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var apiURL string
|
||||||
|
|
||||||
|
apiURL = g.GetPath()
|
||||||
|
|
||||||
|
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 bitgetapi.APIResponse
|
||||||
|
if err := response.DecodeJSON(&apiResponse); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type responseValidator interface {
|
||||||
|
Validate() error
|
||||||
|
}
|
||||||
|
validator, ok := interface{}(apiResponse).(responseValidator)
|
||||||
|
if ok {
|
||||||
|
if err := validator.Validate(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var data KLineResponse
|
||||||
|
if err := json.Unmarshal(apiResponse.Data, &data); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return data, nil
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"math"
|
"math"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/c9s/bbgo/pkg/exchange/bitget/bitgetapi"
|
"github.com/c9s/bbgo/pkg/exchange/bitget/bitgetapi"
|
||||||
v2 "github.com/c9s/bbgo/pkg/exchange/bitget/bitgetapi/v2"
|
v2 "github.com/c9s/bbgo/pkg/exchange/bitget/bitgetapi/v2"
|
||||||
|
@ -341,3 +342,28 @@ func toGlobalBalanceMap(balances []Balance) types.BalanceMap {
|
||||||
}
|
}
|
||||||
return bm
|
return bm
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func toGlobalKLines(symbol string, interval types.Interval, kLines v2.KLineResponse) []types.KLine {
|
||||||
|
gKLines := make([]types.KLine, len(kLines))
|
||||||
|
for i, kline := range kLines {
|
||||||
|
// follow the binance rule, to avoid endTime overlapping with the next startTime. So we subtract -1 time.Millisecond
|
||||||
|
// on endTime.
|
||||||
|
endTime := types.Time(kline.Ts.Time().Add(interval.Duration() - time.Millisecond))
|
||||||
|
gKLines[i] = types.KLine{
|
||||||
|
Exchange: types.ExchangeBitget,
|
||||||
|
Symbol: symbol,
|
||||||
|
StartTime: types.Time(kline.Ts),
|
||||||
|
EndTime: endTime,
|
||||||
|
Interval: interval,
|
||||||
|
Open: kline.Open,
|
||||||
|
Close: kline.Close,
|
||||||
|
High: kline.High,
|
||||||
|
Low: kline.Low,
|
||||||
|
Volume: kline.Volume,
|
||||||
|
QuoteVolume: kline.QuoteVolume,
|
||||||
|
// Bitget doesn't support close flag in REST API
|
||||||
|
Closed: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return gKLines
|
||||||
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package bitget
|
||||||
import (
|
import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
@ -598,3 +599,88 @@ func Test_toGlobalBalanceMap(t *testing.T) {
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_toGlobalKLines(t *testing.T) {
|
||||||
|
symbol := "BTCUSDT"
|
||||||
|
interval := types.Interval15m
|
||||||
|
|
||||||
|
resp := v2.KLineResponse{
|
||||||
|
/*
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"Ts": "1699816800000",
|
||||||
|
"OpenPrice": 29045.3,
|
||||||
|
"HighPrice": 29228.56,
|
||||||
|
"LowPrice": 29045.3,
|
||||||
|
"ClosePrice": 29228.56,
|
||||||
|
"Volume": 9.265593,
|
||||||
|
"QuoteVolume": 270447.43520753,
|
||||||
|
"UsdtVolume": 270447.43520753
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Ts": "1699816800000",
|
||||||
|
"OpenPrice": 29167.33,
|
||||||
|
"HighPrice": 29229.08,
|
||||||
|
"LowPrice": 29000,
|
||||||
|
"ClosePrice": 29045.3,
|
||||||
|
"Volume": 9.295508,
|
||||||
|
"QuoteVolume": 270816.87513775,
|
||||||
|
"UsdtVolume": 270816.87513775
|
||||||
|
}
|
||||||
|
]
|
||||||
|
*/
|
||||||
|
{
|
||||||
|
Ts: 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),
|
||||||
|
QuoteVolume: fixedpoint.NewFromFloat(270447.43520753),
|
||||||
|
UsdtVolume: fixedpoint.NewFromFloat(270447.43520753),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Ts: 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),
|
||||||
|
QuoteVolume: fixedpoint.NewFromFloat(270816.87513775),
|
||||||
|
UsdtVolume: fixedpoint.NewFromFloat(270447.43520753),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
expKlines := []types.KLine{
|
||||||
|
{
|
||||||
|
Exchange: types.ExchangeBitget,
|
||||||
|
Symbol: symbol,
|
||||||
|
StartTime: types.Time(resp[0].Ts.Time()),
|
||||||
|
EndTime: types.Time(resp[0].Ts.Time().Add(interval.Duration() - time.Millisecond)),
|
||||||
|
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.ExchangeBitget,
|
||||||
|
Symbol: symbol,
|
||||||
|
StartTime: types.Time(resp[1].Ts.Time()),
|
||||||
|
EndTime: types.Time(resp[1].Ts.Time().Add(interval.Duration() - time.Millisecond)),
|
||||||
|
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), expKlines)
|
||||||
|
}
|
||||||
|
|
|
@ -21,9 +21,10 @@ const (
|
||||||
|
|
||||||
PlatformToken = "BGB"
|
PlatformToken = "BGB"
|
||||||
|
|
||||||
queryLimit = 100
|
queryLimit = 100
|
||||||
maxOrderIdLen = 36
|
defaultKLineLimit = 100
|
||||||
queryMaxDuration = 90 * 24 * time.Hour
|
maxOrderIdLen = 36
|
||||||
|
queryMaxDuration = 90 * 24 * time.Hour
|
||||||
)
|
)
|
||||||
|
|
||||||
var log = logrus.WithFields(logrus.Fields{
|
var log = logrus.WithFields(logrus.Fields{
|
||||||
|
@ -49,6 +50,8 @@ var (
|
||||||
queryTradeRateLimiter = rate.NewLimiter(rate.Every(time.Second/5), 5)
|
queryTradeRateLimiter = rate.NewLimiter(rate.Every(time.Second/5), 5)
|
||||||
// cancelOrderRateLimiter has its own rate limit. https://www.bitget.com/api-doc/spot/trade/Cancel-Order
|
// cancelOrderRateLimiter has its own rate limit. https://www.bitget.com/api-doc/spot/trade/Cancel-Order
|
||||||
cancelOrderRateLimiter = rate.NewLimiter(rate.Every(time.Second/5), 5)
|
cancelOrderRateLimiter = rate.NewLimiter(rate.Every(time.Second/5), 5)
|
||||||
|
// kLineRateLimiter has its own rate limit. https://www.bitget.com/api-doc/spot/market/Get-Candle-Data
|
||||||
|
kLineOrderRateLimiter = rate.NewLimiter(rate.Every(time.Second/10), 5)
|
||||||
)
|
)
|
||||||
|
|
||||||
type Exchange struct {
|
type Exchange struct {
|
||||||
|
@ -153,9 +156,56 @@ func (e *Exchange) QueryTickers(ctx context.Context, symbols ...string) (map[str
|
||||||
return tickers, nil
|
return tickers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// QueryKLines queries the k line data by interval and time range...etc.
|
||||||
|
//
|
||||||
|
// If you provide only the start time, the system will return the latest data.
|
||||||
|
// If you provide both the start and end times, the system will return data within the specified range.
|
||||||
|
// If you provide only the end time, the system will return data that occurred before the end time.
|
||||||
|
//
|
||||||
|
// The end time has different limits. 1m, 5m can query for one month,15m can query for 52 days,30m can query for 62 days,
|
||||||
|
// 1H can query for 83 days,4H can query for 240 days,6H can query for 360 days.
|
||||||
func (e *Exchange) QueryKLines(ctx context.Context, symbol string, interval types.Interval, options types.KLineQueryOptions) ([]types.KLine, error) {
|
func (e *Exchange) QueryKLines(ctx context.Context, symbol string, interval types.Interval, options types.KLineQueryOptions) ([]types.KLine, error) {
|
||||||
// TODO implement me
|
req := e.v2Client.NewGetKLineRequest().Symbol(symbol)
|
||||||
panic("implement me")
|
intervalStr, found := toLocalGranularity[interval]
|
||||||
|
if !found {
|
||||||
|
return nil, fmt.Errorf("%s not supported, supported granlarity: %+v", intervalStr, toLocalGranularity)
|
||||||
|
}
|
||||||
|
req.Granularity(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(strconv.FormatUint(limit, 10))
|
||||||
|
|
||||||
|
if options.StartTime != nil {
|
||||||
|
req.StartTime(*options.StartTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.EndTime != nil {
|
||||||
|
if options.StartTime != nil && options.EndTime.Before(*options.StartTime) {
|
||||||
|
return nil, fmt.Errorf("end time %s before start time %s", *options.EndTime, *options.StartTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
ok, duration := hasMaxDuration(interval)
|
||||||
|
if ok && time.Since(*options.EndTime) > duration {
|
||||||
|
return nil, fmt.Errorf("end time %s are greater than max duration %s", *options.EndTime, duration)
|
||||||
|
}
|
||||||
|
req.EndTime(*options.EndTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := kLineOrderRateLimiter.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)
|
||||||
|
}
|
||||||
|
|
||||||
|
kLines := toGlobalKLines(symbol, interval, resp)
|
||||||
|
return types.SortKLinesAscending(kLines), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Exchange) QueryAccount(ctx context.Context) (*types.Account, error) {
|
func (e *Exchange) QueryAccount(ctx context.Context) (*types.Account, error) {
|
||||||
|
|
|
@ -299,8 +299,43 @@ var (
|
||||||
"candle1D": types.Interval1d,
|
"candle1D": types.Interval1d,
|
||||||
"candle1W": types.Interval1w,
|
"candle1W": types.Interval1w,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// we align utc time zone
|
||||||
|
toLocalGranularity = map[types.Interval]string{
|
||||||
|
types.Interval1m: "1min",
|
||||||
|
types.Interval5m: "5min",
|
||||||
|
types.Interval15m: "15min",
|
||||||
|
types.Interval30m: "30min",
|
||||||
|
types.Interval1h: "1h",
|
||||||
|
types.Interval4h: "4h",
|
||||||
|
types.Interval6h: "6Hutc",
|
||||||
|
types.Interval12h: "12Hutc",
|
||||||
|
types.Interval1d: "1Dutc",
|
||||||
|
types.Interval3d: "3Dutc",
|
||||||
|
types.Interval1w: "1Wutc",
|
||||||
|
types.Interval1mo: "1Mutc",
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func hasMaxDuration(interval types.Interval) (bool, time.Duration) {
|
||||||
|
switch interval {
|
||||||
|
case types.Interval1m, types.Interval5m:
|
||||||
|
return true, 30 * 24 * time.Hour
|
||||||
|
case types.Interval15m:
|
||||||
|
return true, 52 * 24 * time.Hour
|
||||||
|
case types.Interval30m:
|
||||||
|
return true, 62 * 24 * time.Hour
|
||||||
|
case types.Interval1h:
|
||||||
|
return true, 83 * 24 * time.Hour
|
||||||
|
case types.Interval4h:
|
||||||
|
return true, 240 * 24 * time.Hour
|
||||||
|
case types.Interval6h:
|
||||||
|
return true, 360 * 24 * time.Hour
|
||||||
|
default:
|
||||||
|
return false, 0 * time.Duration(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type KLine struct {
|
type KLine struct {
|
||||||
StartTime types.MillisecondTimestamp
|
StartTime types.MillisecondTimestamp
|
||||||
OpenPrice fixedpoint.Value
|
OpenPrice fixedpoint.Value
|
||||||
|
|
|
@ -6,38 +6,35 @@ import (
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
|
||||||
"github.com/c9s/bbgo/pkg/types"
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestKLine_ToGlobal(t *testing.T) {
|
func Test_hasMaxDuration(t *testing.T) {
|
||||||
startTime := int64(1698744600000)
|
ok, duration := hasMaxDuration(types.Interval1m)
|
||||||
interval := types.Interval1m
|
assert.True(t, ok)
|
||||||
k := KLine{
|
assert.Equal(t, 30*24*time.Hour, duration)
|
||||||
StartTime: types.NewMillisecondTimestampFromInt(startTime),
|
|
||||||
OpenPrice: fixedpoint.NewFromFloat(34361.49),
|
|
||||||
HighestPrice: fixedpoint.NewFromFloat(34458.98),
|
|
||||||
LowestPrice: fixedpoint.NewFromFloat(34355.53),
|
|
||||||
ClosePrice: fixedpoint.NewFromFloat(34416.41),
|
|
||||||
Volume: fixedpoint.NewFromFloat(99.6631),
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, types.KLine{
|
ok, duration = hasMaxDuration(types.Interval5m)
|
||||||
Exchange: types.ExchangeBitget,
|
assert.True(t, ok)
|
||||||
Symbol: "BTCUSDT",
|
assert.Equal(t, 30*24*time.Hour, duration)
|
||||||
StartTime: types.Time(types.NewMillisecondTimestampFromInt(startTime).Time()),
|
|
||||||
EndTime: types.Time(types.NewMillisecondTimestampFromInt(startTime).Time().Add(interval.Duration() - time.Millisecond)),
|
ok, duration = hasMaxDuration(types.Interval15m)
|
||||||
Interval: interval,
|
assert.True(t, ok)
|
||||||
Open: fixedpoint.NewFromFloat(34361.49),
|
assert.Equal(t, 52*24*time.Hour, duration)
|
||||||
Close: fixedpoint.NewFromFloat(34416.41),
|
|
||||||
High: fixedpoint.NewFromFloat(34458.98),
|
ok, duration = hasMaxDuration(types.Interval30m)
|
||||||
Low: fixedpoint.NewFromFloat(34355.53),
|
assert.True(t, ok)
|
||||||
Volume: fixedpoint.NewFromFloat(99.6631),
|
assert.Equal(t, 62*24*time.Hour, duration)
|
||||||
QuoteVolume: fixedpoint.Zero,
|
|
||||||
TakerBuyBaseAssetVolume: fixedpoint.Zero,
|
ok, duration = hasMaxDuration(types.Interval1h)
|
||||||
TakerBuyQuoteAssetVolume: fixedpoint.Zero,
|
assert.True(t, ok)
|
||||||
LastTradeID: 0,
|
assert.Equal(t, 83*24*time.Hour, duration)
|
||||||
NumberOfTrades: 0,
|
|
||||||
Closed: false,
|
ok, duration = hasMaxDuration(types.Interval4h)
|
||||||
}, k.ToGlobal(interval, "BTCUSDT"))
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, 240*24*time.Hour, duration)
|
||||||
|
|
||||||
|
ok, duration = hasMaxDuration(types.Interval6h)
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, 360*24*time.Hour, duration)
|
||||||
}
|
}
|
||||||
|
|
|
@ -368,7 +368,7 @@ func toLocalInterval(interval types.Interval) (string, error) {
|
||||||
func toGlobalKLines(symbol string, interval types.Interval, klines []bybitapi.KLine) []types.KLine {
|
func toGlobalKLines(symbol string, interval types.Interval, klines []bybitapi.KLine) []types.KLine {
|
||||||
gKLines := make([]types.KLine, len(klines))
|
gKLines := make([]types.KLine, len(klines))
|
||||||
for i, kline := range klines {
|
for i, kline := range klines {
|
||||||
endTime := types.Time(kline.StartTime.Time().Add(interval.Duration()))
|
endTime := types.Time(kline.StartTime.Time().Add(interval.Duration() - time.Millisecond))
|
||||||
gKLines[i] = types.KLine{
|
gKLines[i] = types.KLine{
|
||||||
Exchange: types.ExchangeBybit,
|
Exchange: types.ExchangeBybit,
|
||||||
Symbol: symbol,
|
Symbol: symbol,
|
||||||
|
|
|
@ -836,7 +836,7 @@ func Test_toGlobalKLines(t *testing.T) {
|
||||||
Exchange: types.ExchangeBybit,
|
Exchange: types.ExchangeBybit,
|
||||||
Symbol: resp.Symbol,
|
Symbol: resp.Symbol,
|
||||||
StartTime: types.Time(resp.List[0].StartTime.Time()),
|
StartTime: types.Time(resp.List[0].StartTime.Time()),
|
||||||
EndTime: types.Time(resp.List[0].StartTime.Time().Add(interval.Duration())),
|
EndTime: types.Time(resp.List[0].StartTime.Time().Add(interval.Duration() - time.Millisecond)),
|
||||||
Interval: interval,
|
Interval: interval,
|
||||||
Open: fixedpoint.NewFromFloat(29045.3),
|
Open: fixedpoint.NewFromFloat(29045.3),
|
||||||
Close: fixedpoint.NewFromFloat(29228.56),
|
Close: fixedpoint.NewFromFloat(29228.56),
|
||||||
|
@ -850,7 +850,7 @@ func Test_toGlobalKLines(t *testing.T) {
|
||||||
Exchange: types.ExchangeBybit,
|
Exchange: types.ExchangeBybit,
|
||||||
Symbol: resp.Symbol,
|
Symbol: resp.Symbol,
|
||||||
StartTime: types.Time(resp.List[1].StartTime.Time()),
|
StartTime: types.Time(resp.List[1].StartTime.Time()),
|
||||||
EndTime: types.Time(resp.List[1].StartTime.Time().Add(interval.Duration())),
|
EndTime: types.Time(resp.List[1].StartTime.Time().Add(interval.Duration() - time.Millisecond)),
|
||||||
Interval: interval,
|
Interval: interval,
|
||||||
Open: fixedpoint.NewFromFloat(29167.33),
|
Open: fixedpoint.NewFromFloat(29167.33),
|
||||||
Close: fixedpoint.NewFromFloat(29045.3),
|
Close: fixedpoint.NewFromFloat(29045.3),
|
||||||
|
|
|
@ -54,7 +54,9 @@ type KLine struct {
|
||||||
Symbol string `json:"symbol" db:"symbol"`
|
Symbol string `json:"symbol" db:"symbol"`
|
||||||
|
|
||||||
StartTime Time `json:"startTime" db:"start_time"`
|
StartTime Time `json:"startTime" db:"start_time"`
|
||||||
EndTime Time `json:"endTime" db:"end_time"`
|
// EndTime follows the binance rule, to avoid endTime overlapping with the next startTime. So if your end time (2023-01-01 01:00:00)
|
||||||
|
// are overlapping with next start time interval (2023-01-01 01:00:00), you should subtract -1 time.millisecond on EndTime.
|
||||||
|
EndTime Time `json:"endTime" db:"end_time"`
|
||||||
|
|
||||||
Interval Interval `json:"interval" db:"interval"`
|
Interval Interval `json:"interval" db:"interval"`
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user