mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-22 14:55:16 +00:00
Merge pull request #1237 from bailantaotao/edwin/add-new-exchange-account-api
FEATURE: add new exchange Bybit GetAccountInfo/GetInstrumentsInfo api
This commit is contained in:
commit
06a741e615
|
@ -128,7 +128,8 @@ the implementation.
|
|||
- OKEx Spot Exchange
|
||||
- Kucoin Spot Exchange
|
||||
- MAX Spot Exchange (located in Taiwan)
|
||||
- Bitget (In Progress)
|
||||
- Bitget Exchange (In Progress)
|
||||
- Bybit Exchange (In Progress)
|
||||
|
||||
## Documentation and General Topics
|
||||
|
||||
|
@ -219,6 +220,10 @@ KUCOIN_API_KEY=
|
|||
KUCOIN_API_SECRET=
|
||||
KUCOIN_API_PASSPHRASE=
|
||||
KUCOIN_API_KEY_VERSION=2
|
||||
|
||||
# for Bybit exchange, if you have one
|
||||
BYBIT_API_KEY=
|
||||
BYBIT_API_SECRET=
|
||||
```
|
||||
|
||||
Prepare your dotenv file `.env.local` and BBGO yaml config file `bbgo.yaml`.
|
||||
|
|
165
pkg/exchange/bybit/bybitapi/client.go
Normal file
165
pkg/exchange/bybit/bybitapi/client.go
Normal file
|
@ -0,0 +1,165 @@
|
|||
package bybitapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/c9s/requestgen"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
)
|
||||
|
||||
const defaultHTTPTimeout = time.Second * 15
|
||||
const RestBaseURL = "https://api.bybit.com"
|
||||
|
||||
// defaultRequestWindowMilliseconds specify how long an HTTP request is valid. It is also used to prevent replay attacks.
|
||||
var defaultRequestWindowMilliseconds = fmt.Sprintf("%d", time.Millisecond*5000)
|
||||
|
||||
type RestClient struct {
|
||||
requestgen.BaseAPIClient
|
||||
|
||||
key, secret string
|
||||
}
|
||||
|
||||
func NewClient() (*RestClient, error) {
|
||||
u, err := url.Parse(RestBaseURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &RestClient{
|
||||
BaseAPIClient: requestgen.BaseAPIClient{
|
||||
BaseURL: u,
|
||||
HttpClient: &http.Client{
|
||||
Timeout: defaultHTTPTimeout,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *RestClient) Auth(key, secret string) {
|
||||
c.key = key
|
||||
// pragma: allowlist secret
|
||||
c.secret = secret
|
||||
}
|
||||
|
||||
// newAuthenticatedRequest creates new http request for authenticated routes.
|
||||
func (c *RestClient) NewAuthenticatedRequest(ctx context.Context, method, refURL string, params url.Values, payload interface{}) (*http.Request, error) {
|
||||
if len(c.key) == 0 {
|
||||
return nil, errors.New("empty api key")
|
||||
}
|
||||
|
||||
if len(c.secret) == 0 {
|
||||
return nil, errors.New("empty api secret")
|
||||
}
|
||||
|
||||
rel, err := url.Parse(refURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if params != nil {
|
||||
rel.RawQuery = params.Encode()
|
||||
}
|
||||
|
||||
pathURL := c.BaseURL.ResolveReference(rel)
|
||||
path := pathURL.Path
|
||||
if rel.RawQuery != "" {
|
||||
path += "?" + rel.RawQuery
|
||||
}
|
||||
|
||||
t := time.Now().In(time.UTC)
|
||||
timestamp := strconv.FormatInt(t.UnixMilli(), 10)
|
||||
|
||||
body, err := castPayload(payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var signKey string
|
||||
switch method {
|
||||
case http.MethodPost:
|
||||
signKey = timestamp + c.key + defaultRequestWindowMilliseconds + string(body)
|
||||
case http.MethodGet:
|
||||
signKey = timestamp + c.key + defaultRequestWindowMilliseconds + rel.RawQuery
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected method: %s", method)
|
||||
}
|
||||
|
||||
// See https://bybit-exchange.github.io/docs/v5/guide#create-a-request
|
||||
//
|
||||
// 1. timestamp + API key + (recv_window) + (queryString | jsonBodyString)
|
||||
// 2. Use the HMAC_SHA256 or RSA_SHA256 algorithm to sign the string in step 1, and convert it to a hex
|
||||
// string (HMAC_SHA256) / base64 (RSA_SHA256) to obtain the sign parameter.
|
||||
// 3. Append the sign parameter to request header, and send the HTTP request.
|
||||
signature := sign(signKey, c.secret)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, method, pathURL.String(), bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
req.Header.Add("X-BAPI-API-KEY", c.key)
|
||||
req.Header.Add("X-BAPI-TIMESTAMP", timestamp)
|
||||
req.Header.Add("X-BAPI-SIGN", signature)
|
||||
req.Header.Add("X-BAPI-RECV-WINDOW", defaultRequestWindowMilliseconds)
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func sign(payload string, secret string) string {
|
||||
var sig = hmac.New(sha256.New, []byte(secret))
|
||||
_, err := sig.Write([]byte(payload))
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return base64.StdEncoding.EncodeToString(sig.Sum(nil))
|
||||
}
|
||||
|
||||
func castPayload(payload interface{}) ([]byte, error) {
|
||||
if payload == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
switch v := payload.(type) {
|
||||
case string:
|
||||
return []byte(v), nil
|
||||
|
||||
case []byte:
|
||||
return v, nil
|
||||
|
||||
}
|
||||
return json.Marshal(payload)
|
||||
}
|
||||
|
||||
/*
|
||||
sample:
|
||||
|
||||
{
|
||||
"retCode": 0,
|
||||
"retMsg": "OK",
|
||||
"result": {
|
||||
},
|
||||
"retExtInfo": {},
|
||||
"time": 1671017382656
|
||||
}
|
||||
*/
|
||||
|
||||
type APIResponse struct {
|
||||
RetCode uint `json:"retCode"`
|
||||
RetMsg string `json:"retMsg"`
|
||||
Result json.RawMessage `json:"result"`
|
||||
RetExtInfo json.RawMessage `json:"retExtInfo"`
|
||||
Time types.MillisecondTimestamp `json:"time"`
|
||||
}
|
48
pkg/exchange/bybit/bybitapi/client_test.go
Normal file
48
pkg/exchange/bybit/bybitapi/client_test.go
Normal file
|
@ -0,0 +1,48 @@
|
|||
package bybitapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/testutil"
|
||||
)
|
||||
|
||||
func getTestClientOrSkip(t *testing.T) *RestClient {
|
||||
if b, _ := strconv.ParseBool(os.Getenv("CI")); b {
|
||||
t.Skip("skip test for CI")
|
||||
}
|
||||
|
||||
key, secret, ok := testutil.IntegrationTestConfigured(t, "BYBIT")
|
||||
if !ok {
|
||||
t.Skip("BYBIT_* env vars are not configured")
|
||||
return nil
|
||||
}
|
||||
|
||||
client, err := NewClient()
|
||||
assert.NoError(t, err)
|
||||
client.Auth(key, secret)
|
||||
return client
|
||||
}
|
||||
|
||||
func TestClient(t *testing.T) {
|
||||
client := getTestClientOrSkip(t)
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("GetAccountInfoRequest", func(t *testing.T) {
|
||||
req := client.NewGetAccountRequest()
|
||||
accountInfo, err := req.Do(ctx)
|
||||
assert.NoError(t, err)
|
||||
t.Logf("accountInfo: %+v", accountInfo)
|
||||
})
|
||||
|
||||
t.Run("GetInstrumentsInfoRequest", func(t *testing.T) {
|
||||
req := client.NewGetInstrumentsInfoRequest()
|
||||
instrumentsInfo, err := req.Do(ctx)
|
||||
assert.NoError(t, err)
|
||||
t.Logf("instrumentsInfo: %+v", instrumentsInfo)
|
||||
})
|
||||
}
|
24
pkg/exchange/bybit/bybitapi/get_account_info_request.go
Normal file
24
pkg/exchange/bybit/bybitapi/get_account_info_request.go
Normal file
|
@ -0,0 +1,24 @@
|
|||
package bybitapi
|
||||
|
||||
import "github.com/c9s/requestgen"
|
||||
|
||||
//go:generate -command GetRequest requestgen -method GET -responseType .APIResponse -responseDataField Result
|
||||
//go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Result
|
||||
|
||||
type AccountInfo struct {
|
||||
MarginMode string `json:"marginMode"`
|
||||
UpdatedTime string `json:"updatedTime"`
|
||||
UnifiedMarginStatus int `json:"unifiedMarginStatus"`
|
||||
DcpStatus string `json:"dcpStatus"`
|
||||
TimeWindow int `json:"timeWindow"`
|
||||
SmpGroup int `json:"smpGroup"`
|
||||
}
|
||||
|
||||
//go:generate GetRequest -url "/v5/account/info" -type GetAccountInfoRequest -responseDataType .AccountInfo
|
||||
type GetAccountInfoRequest struct {
|
||||
client requestgen.AuthenticatedAPIClient
|
||||
}
|
||||
|
||||
func (c *RestClient) NewGetAccountRequest() *GetAccountInfoRequest {
|
||||
return &GetAccountInfoRequest{client: c}
|
||||
}
|
|
@ -0,0 +1,139 @@
|
|||
// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Result -url /v5/account/info -type GetAccountInfoRequest -responseDataType .AccountInfo"; DO NOT EDIT.
|
||||
|
||||
package bybitapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
// GetQueryParameters builds and checks the query parameters and returns url.Values
|
||||
func (g *GetAccountInfoRequest) GetQueryParameters() (url.Values, error) {
|
||||
var params = map[string]interface{}{}
|
||||
|
||||
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 *GetAccountInfoRequest) 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 *GetAccountInfoRequest) 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 *GetAccountInfoRequest) 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 *GetAccountInfoRequest) GetSlugParameters() (map[string]interface{}, error) {
|
||||
var params = map[string]interface{}{}
|
||||
|
||||
return params, nil
|
||||
}
|
||||
|
||||
func (g *GetAccountInfoRequest) 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 *GetAccountInfoRequest) 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 *GetAccountInfoRequest) isVarSlice(_v interface{}) bool {
|
||||
rt := reflect.TypeOf(_v)
|
||||
switch rt.Kind() {
|
||||
case reflect.Slice:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (g *GetAccountInfoRequest) 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 *GetAccountInfoRequest) Do(ctx context.Context) (*AccountInfo, error) {
|
||||
|
||||
// no body params
|
||||
var params interface{}
|
||||
query := url.Values{}
|
||||
|
||||
apiURL := "/v5/account/info"
|
||||
|
||||
req, err := g.client.NewAuthenticatedRequest(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 AccountInfo
|
||||
if err := json.Unmarshal(apiResponse.Result, &data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &data, nil
|
||||
}
|
51
pkg/exchange/bybit/bybitapi/get_instruments_info_request.go
Normal file
51
pkg/exchange/bybit/bybitapi/get_instruments_info_request.go
Normal file
|
@ -0,0 +1,51 @@
|
|||
package bybitapi
|
||||
|
||||
import (
|
||||
"github.com/c9s/requestgen"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||
)
|
||||
|
||||
//go:generate -command GetRequest requestgen -method GET -responseType .APIResponse -responseDataField Result
|
||||
//go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Result
|
||||
|
||||
type InstrumentsInfo struct {
|
||||
Category Category `json:"category"`
|
||||
List []struct {
|
||||
Symbol string `json:"symbol"`
|
||||
BaseCoin string `json:"baseCoin"`
|
||||
QuoteCoin string `json:"quoteCoin"`
|
||||
Innovation string `json:"innovation"`
|
||||
Status Status `json:"status"`
|
||||
MarginTrading string `json:"marginTrading"`
|
||||
LotSizeFilter struct {
|
||||
BasePrecision fixedpoint.Value `json:"basePrecision"`
|
||||
QuotePrecision fixedpoint.Value `json:"quotePrecision"`
|
||||
MinOrderQty fixedpoint.Value `json:"minOrderQty"`
|
||||
MaxOrderQty fixedpoint.Value `json:"maxOrderQty"`
|
||||
MinOrderAmt fixedpoint.Value `json:"minOrderAmt"`
|
||||
MaxOrderAmt fixedpoint.Value `json:"maxOrderAmt"`
|
||||
} `json:"lotSizeFilter"`
|
||||
|
||||
PriceFilter struct {
|
||||
TickSize fixedpoint.Value `json:"tickSize"`
|
||||
} `json:"priceFilter"`
|
||||
} `json:"list"`
|
||||
}
|
||||
|
||||
//go:generate GetRequest -url "/v5/market/instruments-info" -type GetInstrumentsInfoRequest -responseDataType .InstrumentsInfo
|
||||
type GetInstrumentsInfoRequest struct {
|
||||
client requestgen.APIClient
|
||||
|
||||
category Category `param:"category,query" validValues:"spot"`
|
||||
symbol *string `param:"symbol,query"`
|
||||
limit *uint64 `param:"limit,query"`
|
||||
cursor *string `param:"cursor,query"`
|
||||
}
|
||||
|
||||
func (c *RestClient) NewGetInstrumentsInfoRequest() *GetInstrumentsInfoRequest {
|
||||
return &GetInstrumentsInfoRequest{
|
||||
client: c,
|
||||
category: CategorySpot,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,202 @@
|
|||
// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Result -url /v5/market/instruments-info -type GetInstrumentsInfoRequest -responseDataType .InstrumentsInfo"; DO NOT EDIT.
|
||||
|
||||
package bybitapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
func (g *GetInstrumentsInfoRequest) Category(category Category) *GetInstrumentsInfoRequest {
|
||||
g.category = category
|
||||
return g
|
||||
}
|
||||
|
||||
func (g *GetInstrumentsInfoRequest) Symbol(symbol string) *GetInstrumentsInfoRequest {
|
||||
g.symbol = &symbol
|
||||
return g
|
||||
}
|
||||
|
||||
func (g *GetInstrumentsInfoRequest) Limit(limit uint64) *GetInstrumentsInfoRequest {
|
||||
g.limit = &limit
|
||||
return g
|
||||
}
|
||||
|
||||
func (g *GetInstrumentsInfoRequest) Cursor(cursor string) *GetInstrumentsInfoRequest {
|
||||
g.cursor = &cursor
|
||||
return g
|
||||
}
|
||||
|
||||
// GetQueryParameters builds and checks the query parameters and returns url.Values
|
||||
func (g *GetInstrumentsInfoRequest) 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
|
||||
if g.symbol != nil {
|
||||
symbol := *g.symbol
|
||||
|
||||
// assign parameter of symbol
|
||||
params["symbol"] = symbol
|
||||
} else {
|
||||
}
|
||||
// check limit field -> json key limit
|
||||
if g.limit != nil {
|
||||
limit := *g.limit
|
||||
|
||||
// assign parameter of limit
|
||||
params["limit"] = limit
|
||||
} else {
|
||||
}
|
||||
// check cursor field -> json key cursor
|
||||
if g.cursor != nil {
|
||||
cursor := *g.cursor
|
||||
|
||||
// assign parameter of cursor
|
||||
params["cursor"] = cursor
|
||||
} 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 *GetInstrumentsInfoRequest) 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 *GetInstrumentsInfoRequest) 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 *GetInstrumentsInfoRequest) 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 *GetInstrumentsInfoRequest) GetSlugParameters() (map[string]interface{}, error) {
|
||||
var params = map[string]interface{}{}
|
||||
|
||||
return params, nil
|
||||
}
|
||||
|
||||
func (g *GetInstrumentsInfoRequest) 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 *GetInstrumentsInfoRequest) 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 *GetInstrumentsInfoRequest) isVarSlice(_v interface{}) bool {
|
||||
rt := reflect.TypeOf(_v)
|
||||
switch rt.Kind() {
|
||||
case reflect.Slice:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (g *GetInstrumentsInfoRequest) 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 *GetInstrumentsInfoRequest) Do(ctx context.Context) (*InstrumentsInfo, error) {
|
||||
|
||||
// no body params
|
||||
var params interface{}
|
||||
query, err := g.GetQueryParameters()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
apiURL := "/v5/market/instruments-info"
|
||||
|
||||
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 InstrumentsInfo
|
||||
if err := json.Unmarshal(apiResponse.Result, &data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &data, nil
|
||||
}
|
14
pkg/exchange/bybit/bybitapi/types.go
Normal file
14
pkg/exchange/bybit/bybitapi/types.go
Normal file
|
@ -0,0 +1,14 @@
|
|||
package bybitapi
|
||||
|
||||
type Category string
|
||||
|
||||
const (
|
||||
CategorySpot Category = "spot"
|
||||
)
|
||||
|
||||
type Status string
|
||||
|
||||
const (
|
||||
// StatusTrading is only include the "Trading" status for `spot` category.
|
||||
StatusTrading Status = "Trading"
|
||||
)
|
45
pkg/exchange/bybit/exchange.go
Normal file
45
pkg/exchange/bybit/exchange.go
Normal file
|
@ -0,0 +1,45 @@
|
|||
package bybit
|
||||
|
||||
import (
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/exchange/bybit/bybitapi"
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
)
|
||||
|
||||
var log = logrus.WithFields(logrus.Fields{
|
||||
"exchange": "bybit",
|
||||
})
|
||||
|
||||
type Exchange struct {
|
||||
key, secret string
|
||||
client *bybitapi.RestClient
|
||||
}
|
||||
|
||||
func New(key, secret string) (*Exchange, error) {
|
||||
client, err := bybitapi.NewClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(key) > 0 && len(secret) > 0 {
|
||||
client.Auth(key, secret)
|
||||
}
|
||||
|
||||
return &Exchange{
|
||||
key: key,
|
||||
// pragma: allowlist nextline secret
|
||||
secret: secret,
|
||||
client: client,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (e *Exchange) Name() types.ExchangeName {
|
||||
return types.ExchangeBybit
|
||||
}
|
||||
|
||||
// PlatformFeeCurrency returns empty string. The platform does not support "PlatformFeeCurrency" but instead charges
|
||||
// fees using the native token.
|
||||
func (e *Exchange) PlatformFeeCurrency() string {
|
||||
return ""
|
||||
}
|
|
@ -7,6 +7,7 @@ import (
|
|||
|
||||
"github.com/c9s/bbgo/pkg/exchange/binance"
|
||||
"github.com/c9s/bbgo/pkg/exchange/bitget"
|
||||
"github.com/c9s/bbgo/pkg/exchange/bybit"
|
||||
"github.com/c9s/bbgo/pkg/exchange/kucoin"
|
||||
"github.com/c9s/bbgo/pkg/exchange/max"
|
||||
"github.com/c9s/bbgo/pkg/exchange/okex"
|
||||
|
@ -44,6 +45,9 @@ func New(n types.ExchangeName, key, secret, passphrase string) (types.ExchangeMi
|
|||
case types.ExchangeBitget:
|
||||
return bitget.New(key, secret, passphrase), nil
|
||||
|
||||
case types.ExchangeBybit:
|
||||
return bybit.New(key, secret)
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported exchange: %v", n)
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ var queryOrderLimiter = rate.NewLimiter(rate.Every(6*time.Second), 1)
|
|||
|
||||
var ErrMissingSequence = errors.New("sequence is missing")
|
||||
|
||||
// OKB is the platform currency of OKEx, pre-allocate static string here
|
||||
// KCS is the platform currency of Kucoin, pre-allocate static string here
|
||||
const KCS = "KCS"
|
||||
|
||||
var log = logrus.WithFields(logrus.Fields{
|
||||
|
@ -38,7 +38,6 @@ type Exchange struct {
|
|||
func New(key, secret, passphrase string) *Exchange {
|
||||
client := kucoinapi.NewClient()
|
||||
|
||||
// for public access mode
|
||||
if len(key) > 0 && len(secret) > 0 && len(passphrase) > 0 {
|
||||
client.Auth(key, secret, passphrase)
|
||||
}
|
||||
|
|
|
@ -46,6 +46,7 @@ const (
|
|||
ExchangeKucoin ExchangeName = "kucoin"
|
||||
ExchangeBitget ExchangeName = "bitget"
|
||||
ExchangeBacktest ExchangeName = "backtest"
|
||||
ExchangeBybit ExchangeName = "bybit"
|
||||
)
|
||||
|
||||
var SupportedExchanges = []ExchangeName{
|
||||
|
@ -54,6 +55,7 @@ var SupportedExchanges = []ExchangeName{
|
|||
ExchangeOKEx,
|
||||
ExchangeKucoin,
|
||||
ExchangeBitget,
|
||||
ExchangeBybit,
|
||||
// note: we are not using "backtest"
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user