mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-13 02:23:51 +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
|
- OKEx Spot Exchange
|
||||||
- Kucoin Spot Exchange
|
- Kucoin Spot Exchange
|
||||||
- MAX Spot Exchange (located in Taiwan)
|
- MAX Spot Exchange (located in Taiwan)
|
||||||
- Bitget (In Progress)
|
- Bitget Exchange (In Progress)
|
||||||
|
- Bybit Exchange (In Progress)
|
||||||
|
|
||||||
## Documentation and General Topics
|
## Documentation and General Topics
|
||||||
|
|
||||||
|
@ -219,6 +220,10 @@ KUCOIN_API_KEY=
|
||||||
KUCOIN_API_SECRET=
|
KUCOIN_API_SECRET=
|
||||||
KUCOIN_API_PASSPHRASE=
|
KUCOIN_API_PASSPHRASE=
|
||||||
KUCOIN_API_KEY_VERSION=2
|
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`.
|
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/binance"
|
||||||
"github.com/c9s/bbgo/pkg/exchange/bitget"
|
"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/kucoin"
|
||||||
"github.com/c9s/bbgo/pkg/exchange/max"
|
"github.com/c9s/bbgo/pkg/exchange/max"
|
||||||
"github.com/c9s/bbgo/pkg/exchange/okex"
|
"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:
|
case types.ExchangeBitget:
|
||||||
return bitget.New(key, secret, passphrase), nil
|
return bitget.New(key, secret, passphrase), nil
|
||||||
|
|
||||||
|
case types.ExchangeBybit:
|
||||||
|
return bybit.New(key, secret)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unsupported exchange: %v", n)
|
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")
|
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"
|
const KCS = "KCS"
|
||||||
|
|
||||||
var log = logrus.WithFields(logrus.Fields{
|
var log = logrus.WithFields(logrus.Fields{
|
||||||
|
@ -38,7 +38,6 @@ type Exchange struct {
|
||||||
func New(key, secret, passphrase string) *Exchange {
|
func New(key, secret, passphrase string) *Exchange {
|
||||||
client := kucoinapi.NewClient()
|
client := kucoinapi.NewClient()
|
||||||
|
|
||||||
// for public access mode
|
|
||||||
if len(key) > 0 && len(secret) > 0 && len(passphrase) > 0 {
|
if len(key) > 0 && len(secret) > 0 && len(passphrase) > 0 {
|
||||||
client.Auth(key, secret, passphrase)
|
client.Auth(key, secret, passphrase)
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,6 +46,7 @@ const (
|
||||||
ExchangeKucoin ExchangeName = "kucoin"
|
ExchangeKucoin ExchangeName = "kucoin"
|
||||||
ExchangeBitget ExchangeName = "bitget"
|
ExchangeBitget ExchangeName = "bitget"
|
||||||
ExchangeBacktest ExchangeName = "backtest"
|
ExchangeBacktest ExchangeName = "backtest"
|
||||||
|
ExchangeBybit ExchangeName = "bybit"
|
||||||
)
|
)
|
||||||
|
|
||||||
var SupportedExchanges = []ExchangeName{
|
var SupportedExchanges = []ExchangeName{
|
||||||
|
@ -54,6 +55,7 @@ var SupportedExchanges = []ExchangeName{
|
||||||
ExchangeOKEx,
|
ExchangeOKEx,
|
||||||
ExchangeKucoin,
|
ExchangeKucoin,
|
||||||
ExchangeBitget,
|
ExchangeBitget,
|
||||||
|
ExchangeBybit,
|
||||||
// note: we are not using "backtest"
|
// note: we are not using "backtest"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user