mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-22 14:55:16 +00:00
Merge pull request #1248 from bailantaotao/edwin/add-query-open-orders
pkg/exchange: add QueryOpenOrders API for bybit
This commit is contained in:
commit
3fd66199d7
|
@ -5,7 +5,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/hmac"
|
"crypto/hmac"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/base64"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -23,7 +23,7 @@ const defaultHTTPTimeout = time.Second * 15
|
||||||
const RestBaseURL = "https://api.bybit.com"
|
const RestBaseURL = "https://api.bybit.com"
|
||||||
|
|
||||||
// defaultRequestWindowMilliseconds specify how long an HTTP request is valid. It is also used to prevent replay attacks.
|
// 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)
|
var defaultRequestWindowMilliseconds = fmt.Sprintf("%d", 5*time.Second.Milliseconds())
|
||||||
|
|
||||||
type RestClient struct {
|
type RestClient struct {
|
||||||
requestgen.BaseAPIClient
|
requestgen.BaseAPIClient
|
||||||
|
@ -124,7 +124,7 @@ func sign(payload string, secret string) string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
return base64.StdEncoding.EncodeToString(sig.Sum(nil))
|
return hex.EncodeToString(sig.Sum(nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
func castPayload(payload interface{}) ([]byte, error) {
|
func castPayload(payload interface{}) ([]byte, error) {
|
||||||
|
|
|
@ -57,4 +57,24 @@ func TestClient(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
t.Logf("tickers: %+v", tickers)
|
t.Logf("tickers: %+v", tickers)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("GetOpenOrderRequest", func(t *testing.T) {
|
||||||
|
cursor := ""
|
||||||
|
for {
|
||||||
|
req := client.NewGetOpenOrderRequest().Limit(1)
|
||||||
|
if len(cursor) != 0 {
|
||||||
|
req = req.Cursor(cursor)
|
||||||
|
}
|
||||||
|
openOrders, err := req.Do(ctx)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
for _, o := range openOrders.List {
|
||||||
|
t.Logf("openOrders: %+v", o)
|
||||||
|
}
|
||||||
|
if len(openOrders.NextPageCursor) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
cursor = openOrders.NextPageCursor
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
83
pkg/exchange/bybit/bybitapi/get_open_order_request.go
Normal file
83
pkg/exchange/bybit/bybitapi/get_open_order_request.go
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
package bybitapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||||
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
|
"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 OpenOrdersResponse struct {
|
||||||
|
List []OpenOrder `json:"list"`
|
||||||
|
NextPageCursor string `json:"nextPageCursor"`
|
||||||
|
Category string `json:"category"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OpenOrder struct {
|
||||||
|
OrderId string `json:"orderId"`
|
||||||
|
OrderLinkId string `json:"orderLinkId"`
|
||||||
|
BlockTradeId string `json:"blockTradeId"`
|
||||||
|
Symbol string `json:"symbol"`
|
||||||
|
Price fixedpoint.Value `json:"price"`
|
||||||
|
Qty fixedpoint.Value `json:"qty"`
|
||||||
|
Side Side `json:"side"`
|
||||||
|
IsLeverage string `json:"isLeverage"`
|
||||||
|
PositionIdx int `json:"positionIdx"`
|
||||||
|
OrderStatus OrderStatus `json:"orderStatus"`
|
||||||
|
CancelType string `json:"cancelType"`
|
||||||
|
RejectReason string `json:"rejectReason"`
|
||||||
|
AvgPrice fixedpoint.Value `json:"avgPrice"`
|
||||||
|
LeavesQty fixedpoint.Value `json:"leavesQty"`
|
||||||
|
LeavesValue fixedpoint.Value `json:"leavesValue"`
|
||||||
|
CumExecQty fixedpoint.Value `json:"cumExecQty"`
|
||||||
|
CumExecValue fixedpoint.Value `json:"cumExecValue"`
|
||||||
|
CumExecFee fixedpoint.Value `json:"cumExecFee"`
|
||||||
|
TimeInForce TimeInForce `json:"timeInForce"`
|
||||||
|
OrderType OrderType `json:"orderType"`
|
||||||
|
StopOrderType string `json:"stopOrderType"`
|
||||||
|
OrderIv string `json:"orderIv"`
|
||||||
|
TriggerPrice fixedpoint.Value `json:"triggerPrice"`
|
||||||
|
TakeProfit fixedpoint.Value `json:"takeProfit"`
|
||||||
|
StopLoss fixedpoint.Value `json:"stopLoss"`
|
||||||
|
TpTriggerBy string `json:"tpTriggerBy"`
|
||||||
|
SlTriggerBy string `json:"slTriggerBy"`
|
||||||
|
TriggerDirection int `json:"triggerDirection"`
|
||||||
|
TriggerBy string `json:"triggerBy"`
|
||||||
|
LastPriceOnCreated string `json:"lastPriceOnCreated"`
|
||||||
|
ReduceOnly bool `json:"reduceOnly"`
|
||||||
|
CloseOnTrigger bool `json:"closeOnTrigger"`
|
||||||
|
SmpType string `json:"smpType"`
|
||||||
|
SmpGroup int `json:"smpGroup"`
|
||||||
|
SmpOrderId string `json:"smpOrderId"`
|
||||||
|
TpslMode string `json:"tpslMode"`
|
||||||
|
TpLimitPrice string `json:"tpLimitPrice"`
|
||||||
|
SlLimitPrice string `json:"slLimitPrice"`
|
||||||
|
PlaceType string `json:"placeType"`
|
||||||
|
CreatedTime types.MillisecondTimestamp `json:"createdTime"`
|
||||||
|
UpdatedTime types.MillisecondTimestamp `json:"updatedTime"`
|
||||||
|
}
|
||||||
|
|
||||||
|
//go:generate GetRequest -url "/v5/order/realtime" -type GetOpenOrdersRequest -responseDataType .OpenOrdersResponse
|
||||||
|
type GetOpenOrdersRequest struct {
|
||||||
|
client requestgen.AuthenticatedAPIClient
|
||||||
|
|
||||||
|
category Category `param:"category,query" validValues:"spot"`
|
||||||
|
symbol *string `param:"symbol,query"`
|
||||||
|
baseCoin *string `param:"baseCoin,query"`
|
||||||
|
settleCoin *string `param:"settleCoin,query"`
|
||||||
|
orderId *string `param:"orderId,query"`
|
||||||
|
orderLinkId *string `param:"orderLinkId,query"`
|
||||||
|
openOnly *OpenOnly `param:"openOnly,query" validValues:"0"`
|
||||||
|
orderFilter *string `param:"orderFilter,query"`
|
||||||
|
limit *uint64 `param:"limit,query"`
|
||||||
|
cursor *string `param:"cursor,query"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *RestClient) NewGetOpenOrderRequest() *GetOpenOrdersRequest {
|
||||||
|
return &GetOpenOrdersRequest{
|
||||||
|
client: c,
|
||||||
|
category: CategorySpot,
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,291 @@
|
||||||
|
// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Result -url /v5/order/realtime -type GetOpenOrdersRequest -responseDataType .OpenOrdersResponse"; DO NOT EDIT.
|
||||||
|
|
||||||
|
package bybitapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"reflect"
|
||||||
|
"regexp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (g *GetOpenOrdersRequest) Category(category Category) *GetOpenOrdersRequest {
|
||||||
|
g.category = category
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GetOpenOrdersRequest) Symbol(symbol string) *GetOpenOrdersRequest {
|
||||||
|
g.symbol = &symbol
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GetOpenOrdersRequest) BaseCoin(baseCoin string) *GetOpenOrdersRequest {
|
||||||
|
g.baseCoin = &baseCoin
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GetOpenOrdersRequest) SettleCoin(settleCoin string) *GetOpenOrdersRequest {
|
||||||
|
g.settleCoin = &settleCoin
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GetOpenOrdersRequest) OrderId(orderId string) *GetOpenOrdersRequest {
|
||||||
|
g.orderId = &orderId
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GetOpenOrdersRequest) OrderLinkId(orderLinkId string) *GetOpenOrdersRequest {
|
||||||
|
g.orderLinkId = &orderLinkId
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GetOpenOrdersRequest) OpenOnly(openOnly OpenOnly) *GetOpenOrdersRequest {
|
||||||
|
g.openOnly = &openOnly
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GetOpenOrdersRequest) OrderFilter(orderFilter string) *GetOpenOrdersRequest {
|
||||||
|
g.orderFilter = &orderFilter
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GetOpenOrdersRequest) Limit(limit uint64) *GetOpenOrdersRequest {
|
||||||
|
g.limit = &limit
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GetOpenOrdersRequest) Cursor(cursor string) *GetOpenOrdersRequest {
|
||||||
|
g.cursor = &cursor
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetQueryParameters builds and checks the query parameters and returns url.Values
|
||||||
|
func (g *GetOpenOrdersRequest) 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 baseCoin field -> json key baseCoin
|
||||||
|
if g.baseCoin != nil {
|
||||||
|
baseCoin := *g.baseCoin
|
||||||
|
|
||||||
|
// assign parameter of baseCoin
|
||||||
|
params["baseCoin"] = baseCoin
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
// check settleCoin field -> json key settleCoin
|
||||||
|
if g.settleCoin != nil {
|
||||||
|
settleCoin := *g.settleCoin
|
||||||
|
|
||||||
|
// assign parameter of settleCoin
|
||||||
|
params["settleCoin"] = settleCoin
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
// check orderId field -> json key orderId
|
||||||
|
if g.orderId != nil {
|
||||||
|
orderId := *g.orderId
|
||||||
|
|
||||||
|
// assign parameter of orderId
|
||||||
|
params["orderId"] = orderId
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
// check orderLinkId field -> json key orderLinkId
|
||||||
|
if g.orderLinkId != nil {
|
||||||
|
orderLinkId := *g.orderLinkId
|
||||||
|
|
||||||
|
// assign parameter of orderLinkId
|
||||||
|
params["orderLinkId"] = orderLinkId
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
// check openOnly field -> json key openOnly
|
||||||
|
if g.openOnly != nil {
|
||||||
|
openOnly := *g.openOnly
|
||||||
|
|
||||||
|
// TEMPLATE check-valid-values
|
||||||
|
switch openOnly {
|
||||||
|
case OpenOnlyOrder:
|
||||||
|
params["openOnly"] = openOnly
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("openOnly value %v is invalid", openOnly)
|
||||||
|
|
||||||
|
}
|
||||||
|
// END TEMPLATE check-valid-values
|
||||||
|
|
||||||
|
// assign parameter of openOnly
|
||||||
|
params["openOnly"] = openOnly
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
// check orderFilter field -> json key orderFilter
|
||||||
|
if g.orderFilter != nil {
|
||||||
|
orderFilter := *g.orderFilter
|
||||||
|
|
||||||
|
// assign parameter of orderFilter
|
||||||
|
params["orderFilter"] = orderFilter
|
||||||
|
} 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 *GetOpenOrdersRequest) 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 *GetOpenOrdersRequest) 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 *GetOpenOrdersRequest) 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 *GetOpenOrdersRequest) GetSlugParameters() (map[string]interface{}, error) {
|
||||||
|
var params = map[string]interface{}{}
|
||||||
|
|
||||||
|
return params, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GetOpenOrdersRequest) 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 *GetOpenOrdersRequest) 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 *GetOpenOrdersRequest) isVarSlice(_v interface{}) bool {
|
||||||
|
rt := reflect.TypeOf(_v)
|
||||||
|
switch rt.Kind() {
|
||||||
|
case reflect.Slice:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GetOpenOrdersRequest) 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 *GetOpenOrdersRequest) Do(ctx context.Context) (*OpenOrdersResponse, error) {
|
||||||
|
|
||||||
|
// no body params
|
||||||
|
var params interface{}
|
||||||
|
query, err := g.GetQueryParameters()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
apiURL := "/v5/order/realtime"
|
||||||
|
|
||||||
|
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 OpenOrdersResponse
|
||||||
|
if err := json.Unmarshal(apiResponse.Result, &data); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &data, nil
|
||||||
|
}
|
|
@ -12,3 +12,78 @@ const (
|
||||||
// StatusTrading is only include the "Trading" status for `spot` category.
|
// StatusTrading is only include the "Trading" status for `spot` category.
|
||||||
StatusTrading Status = "Trading"
|
StatusTrading Status = "Trading"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type OpenOnly int
|
||||||
|
|
||||||
|
const (
|
||||||
|
OpenOnlyOrder OpenOnly = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
type Side string
|
||||||
|
|
||||||
|
const (
|
||||||
|
SideBuy Side = "Buy"
|
||||||
|
SideSell Side = "Sell"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OrderStatus string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// OrderStatusCreated order has been accepted by the system but not yet put through the matching engine
|
||||||
|
OrderStatusCreated OrderStatus = "Created"
|
||||||
|
// OrderStatusNew is order has been placed successfully.
|
||||||
|
OrderStatusNew OrderStatus = "New"
|
||||||
|
OrderStatusRejected OrderStatus = "Rejected"
|
||||||
|
OrderStatusPartiallyFilled OrderStatus = "PartiallyFilled"
|
||||||
|
OrderStatusPartiallyFilledCanceled OrderStatus = "PartiallyFilledCanceled"
|
||||||
|
OrderStatusFilled OrderStatus = "Filled"
|
||||||
|
OrderStatusCancelled OrderStatus = "Cancelled"
|
||||||
|
|
||||||
|
// Following statuses is conditional orders. Once you place conditional orders, it will be in untriggered status.
|
||||||
|
// Untriggered -> Triggered -> New
|
||||||
|
// Once the trigger price reached, order status will be moved to triggered
|
||||||
|
// Singe BBGO not support Untriggered/Triggered, so comment it.
|
||||||
|
//
|
||||||
|
// OrderStatusUntriggered means that the order not triggered
|
||||||
|
//OrderStatusUntriggered OrderStatus = "Untriggered"
|
||||||
|
//// OrderStatusTriggered means that the order has been triggered
|
||||||
|
//OrderStatusTriggered OrderStatus = "Triggered"
|
||||||
|
|
||||||
|
// Following statuses is stop orders
|
||||||
|
// OrderStatusDeactivated is an order status for stopOrders.
|
||||||
|
//e.g. when you place a conditional order, then you cancel it, this order status is "Deactivated"
|
||||||
|
OrderStatusDeactivated OrderStatus = "Deactivated"
|
||||||
|
|
||||||
|
// OrderStatusActive order has been triggered and the new active order has been successfully placed. Is the final
|
||||||
|
// state of a successful conditional order
|
||||||
|
OrderStatusActive OrderStatus = "Active"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
AllOrderStatuses = []OrderStatus{
|
||||||
|
OrderStatusCreated,
|
||||||
|
OrderStatusNew,
|
||||||
|
OrderStatusRejected,
|
||||||
|
OrderStatusPartiallyFilled,
|
||||||
|
OrderStatusPartiallyFilledCanceled,
|
||||||
|
OrderStatusFilled,
|
||||||
|
OrderStatusCancelled,
|
||||||
|
OrderStatusDeactivated,
|
||||||
|
OrderStatusActive,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
type OrderType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
OrderTypeMarket OrderType = "Market"
|
||||||
|
OrderTypeLimit OrderType = "Limit"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TimeInForce string
|
||||||
|
|
||||||
|
const (
|
||||||
|
TimeInForceGTC TimeInForce = "GTC"
|
||||||
|
TimeInForceIOC TimeInForce = "IOC"
|
||||||
|
TimeInForceFOK TimeInForce = "FOK"
|
||||||
|
)
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package bybit
|
package bybit
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"hash/fnv"
|
||||||
"math"
|
"math"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -43,3 +45,128 @@ func toGlobalTicker(stats bybitapi.Ticker, time time.Time) types.Ticker {
|
||||||
Time: time,
|
Time: time,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func toGlobalOrder(order bybitapi.OpenOrder) (*types.Order, error) {
|
||||||
|
side, err := toGlobalSideType(order.Side)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
orderType, err := toGlobalOrderType(order.OrderType)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
timeInForce, err := toGlobalTimeInForce(order.TimeInForce)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
status, err := toGlobalOrderStatus(order.OrderStatus)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
working, err := isWorking(order.OrderStatus)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &types.Order{
|
||||||
|
SubmitOrder: types.SubmitOrder{
|
||||||
|
ClientOrderID: order.OrderLinkId,
|
||||||
|
Symbol: order.Symbol,
|
||||||
|
Side: side,
|
||||||
|
Type: orderType,
|
||||||
|
Quantity: order.Qty,
|
||||||
|
Price: order.Price,
|
||||||
|
TimeInForce: timeInForce,
|
||||||
|
},
|
||||||
|
Exchange: types.ExchangeBybit,
|
||||||
|
OrderID: hashStringID(order.OrderId),
|
||||||
|
UUID: order.OrderId,
|
||||||
|
Status: status,
|
||||||
|
ExecutedQuantity: order.CumExecQty,
|
||||||
|
IsWorking: working,
|
||||||
|
CreationTime: types.Time(order.CreatedTime.Time()),
|
||||||
|
UpdateTime: types.Time(order.UpdatedTime.Time()),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func toGlobalSideType(side bybitapi.Side) (types.SideType, error) {
|
||||||
|
switch side {
|
||||||
|
case bybitapi.SideBuy:
|
||||||
|
return types.SideTypeBuy, nil
|
||||||
|
|
||||||
|
case bybitapi.SideSell:
|
||||||
|
return types.SideTypeSell, nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
return types.SideType(side), fmt.Errorf("unexpected side: %s", side)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toGlobalOrderType(s bybitapi.OrderType) (types.OrderType, error) {
|
||||||
|
switch s {
|
||||||
|
case bybitapi.OrderTypeMarket:
|
||||||
|
return types.OrderTypeMarket, nil
|
||||||
|
|
||||||
|
case bybitapi.OrderTypeLimit:
|
||||||
|
return types.OrderTypeLimit, nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
return types.OrderType(s), fmt.Errorf("unexpected order type: %s", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toGlobalTimeInForce(force bybitapi.TimeInForce) (types.TimeInForce, error) {
|
||||||
|
switch force {
|
||||||
|
case bybitapi.TimeInForceGTC:
|
||||||
|
return types.TimeInForceGTC, nil
|
||||||
|
|
||||||
|
case bybitapi.TimeInForceIOC:
|
||||||
|
return types.TimeInForceIOC, nil
|
||||||
|
|
||||||
|
case bybitapi.TimeInForceFOK:
|
||||||
|
return types.TimeInForceFOK, nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
return types.TimeInForce(force), fmt.Errorf("unexpected timeInForce type: %s", force)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toGlobalOrderStatus(status bybitapi.OrderStatus) (types.OrderStatus, error) {
|
||||||
|
switch status {
|
||||||
|
case bybitapi.OrderStatusCreated,
|
||||||
|
bybitapi.OrderStatusNew,
|
||||||
|
bybitapi.OrderStatusActive:
|
||||||
|
return types.OrderStatusNew, nil
|
||||||
|
|
||||||
|
case bybitapi.OrderStatusFilled:
|
||||||
|
return types.OrderStatusFilled, nil
|
||||||
|
|
||||||
|
case bybitapi.OrderStatusPartiallyFilled:
|
||||||
|
return types.OrderStatusPartiallyFilled, nil
|
||||||
|
|
||||||
|
case bybitapi.OrderStatusCancelled,
|
||||||
|
bybitapi.OrderStatusPartiallyFilledCanceled,
|
||||||
|
bybitapi.OrderStatusDeactivated:
|
||||||
|
return types.OrderStatusCanceled, nil
|
||||||
|
|
||||||
|
case bybitapi.OrderStatusRejected:
|
||||||
|
return types.OrderStatusRejected, nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
// following not supported
|
||||||
|
// bybitapi.OrderStatusUntriggered
|
||||||
|
// bybitapi.OrderStatusTriggered
|
||||||
|
return types.OrderStatus(status), fmt.Errorf("unexpected order status: %s", status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func hashStringID(s string) uint64 {
|
||||||
|
h := fnv.New64a()
|
||||||
|
h.Write([]byte(s))
|
||||||
|
return h.Sum64()
|
||||||
|
}
|
||||||
|
|
||||||
|
func isWorking(status bybitapi.OrderStatus) (bool, error) {
|
||||||
|
s, err := toGlobalOrderStatus(status)
|
||||||
|
return s == types.OrderStatusNew || s == types.OrderStatusPartiallyFilled, err
|
||||||
|
}
|
||||||
|
|
|
@ -130,3 +130,229 @@ func TestToGlobalTicker(t *testing.T) {
|
||||||
|
|
||||||
assert.Equal(t, toGlobalTicker(ticker, timeNow), exp)
|
assert.Equal(t, toGlobalTicker(ticker, timeNow), exp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestToGlobalOrder(t *testing.T) {
|
||||||
|
// sample: partialFilled
|
||||||
|
//{
|
||||||
|
// "OrderId": 1472539279335923200,
|
||||||
|
// "OrderLinkId": 1690276361150,
|
||||||
|
// "BlockTradeId": null,
|
||||||
|
// "Symbol": "DOTUSDT",
|
||||||
|
// "Price": 7.278,
|
||||||
|
// "Qty": 0.8,
|
||||||
|
// "Side": "Sell",
|
||||||
|
// "IsLeverage": 0,
|
||||||
|
// "PositionIdx": 0,
|
||||||
|
// "OrderStatus": "PartiallyFilled",
|
||||||
|
// "CancelType": "UNKNOWN",
|
||||||
|
// "RejectReason": null,
|
||||||
|
// "AvgPrice": 7.278,
|
||||||
|
// "LeavesQty": 0,
|
||||||
|
// "LeavesValue": 0,
|
||||||
|
// "CumExecQty": 0.5,
|
||||||
|
// "CumExecValue": 0,
|
||||||
|
// "CumExecFee": 0,
|
||||||
|
// "TimeInForce": "GTC",
|
||||||
|
// "OrderType": "Limit",
|
||||||
|
// "StopOrderType": null,
|
||||||
|
// "OrderIv": null,
|
||||||
|
// "TriggerPrice": 0,
|
||||||
|
// "TakeProfit": 0,
|
||||||
|
// "StopLoss": 0,
|
||||||
|
// "TpTriggerBy": null,
|
||||||
|
// "SlTriggerBy": null,
|
||||||
|
// "TriggerDirection": 0,
|
||||||
|
// "TriggerBy": null,
|
||||||
|
// "LastPriceOnCreated": null,
|
||||||
|
// "ReduceOnly": false,
|
||||||
|
// "CloseOnTrigger": false,
|
||||||
|
// "SmpType": "None",
|
||||||
|
// "SmpGroup": 0,
|
||||||
|
// "SmpOrderId": null,
|
||||||
|
// "TpslMode": null,
|
||||||
|
// "TpLimitPrice": null,
|
||||||
|
// "SlLimitPrice": null,
|
||||||
|
// "PlaceType": null,
|
||||||
|
// "CreatedTime": "2023-07-25 17:12:41.325 +0800 CST",
|
||||||
|
// "UpdatedTime": "2023-07-25 17:12:57.868 +0800 CST"
|
||||||
|
//}
|
||||||
|
timeNow := time.Now()
|
||||||
|
openOrder := bybitapi.OpenOrder{
|
||||||
|
OrderId: "1472539279335923200",
|
||||||
|
OrderLinkId: "1690276361150",
|
||||||
|
BlockTradeId: "",
|
||||||
|
Symbol: "DOTUSDT",
|
||||||
|
Price: fixedpoint.NewFromFloat(7.278),
|
||||||
|
Qty: fixedpoint.NewFromFloat(0.8),
|
||||||
|
Side: bybitapi.SideSell,
|
||||||
|
IsLeverage: "0",
|
||||||
|
PositionIdx: 0,
|
||||||
|
OrderStatus: bybitapi.OrderStatusPartiallyFilled,
|
||||||
|
CancelType: "UNKNOWN",
|
||||||
|
RejectReason: "",
|
||||||
|
AvgPrice: fixedpoint.NewFromFloat(7.728),
|
||||||
|
LeavesQty: fixedpoint.NewFromFloat(0),
|
||||||
|
LeavesValue: fixedpoint.NewFromFloat(0),
|
||||||
|
CumExecQty: fixedpoint.NewFromFloat(0.5),
|
||||||
|
CumExecValue: fixedpoint.NewFromFloat(0),
|
||||||
|
CumExecFee: fixedpoint.NewFromFloat(0),
|
||||||
|
TimeInForce: "GTC",
|
||||||
|
OrderType: bybitapi.OrderTypeLimit,
|
||||||
|
StopOrderType: "",
|
||||||
|
OrderIv: "",
|
||||||
|
TriggerPrice: fixedpoint.NewFromFloat(0),
|
||||||
|
TakeProfit: fixedpoint.NewFromFloat(0),
|
||||||
|
StopLoss: fixedpoint.NewFromFloat(0),
|
||||||
|
TpTriggerBy: "",
|
||||||
|
SlTriggerBy: "",
|
||||||
|
TriggerDirection: 0,
|
||||||
|
TriggerBy: "",
|
||||||
|
LastPriceOnCreated: "",
|
||||||
|
ReduceOnly: false,
|
||||||
|
CloseOnTrigger: false,
|
||||||
|
SmpType: "None",
|
||||||
|
SmpGroup: 0,
|
||||||
|
SmpOrderId: "",
|
||||||
|
TpslMode: "",
|
||||||
|
TpLimitPrice: "",
|
||||||
|
SlLimitPrice: "",
|
||||||
|
PlaceType: "",
|
||||||
|
CreatedTime: types.MillisecondTimestamp(timeNow),
|
||||||
|
UpdatedTime: types.MillisecondTimestamp(timeNow),
|
||||||
|
}
|
||||||
|
side, err := toGlobalSideType(openOrder.Side)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
orderType, err := toGlobalOrderType(openOrder.OrderType)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
tif, err := toGlobalTimeInForce(openOrder.TimeInForce)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
status, err := toGlobalOrderStatus(openOrder.OrderStatus)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
working, err := isWorking(openOrder.OrderStatus)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
exp := types.Order{
|
||||||
|
SubmitOrder: types.SubmitOrder{
|
||||||
|
ClientOrderID: openOrder.OrderLinkId,
|
||||||
|
Symbol: openOrder.Symbol,
|
||||||
|
Side: side,
|
||||||
|
Type: orderType,
|
||||||
|
Quantity: openOrder.Qty,
|
||||||
|
Price: openOrder.Price,
|
||||||
|
TimeInForce: tif,
|
||||||
|
},
|
||||||
|
Exchange: types.ExchangeBybit,
|
||||||
|
OrderID: hashStringID(openOrder.OrderId),
|
||||||
|
UUID: openOrder.OrderId,
|
||||||
|
Status: status,
|
||||||
|
ExecutedQuantity: openOrder.CumExecQty,
|
||||||
|
IsWorking: working,
|
||||||
|
CreationTime: types.Time(timeNow),
|
||||||
|
UpdateTime: types.Time(timeNow),
|
||||||
|
IsFutures: false,
|
||||||
|
IsMargin: false,
|
||||||
|
IsIsolated: false,
|
||||||
|
}
|
||||||
|
res, err := toGlobalOrder(openOrder)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, res, &exp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestToGlobalSideType(t *testing.T) {
|
||||||
|
res, err := toGlobalSideType(bybitapi.SideBuy)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, types.SideTypeBuy, res)
|
||||||
|
|
||||||
|
res, err = toGlobalSideType(bybitapi.SideSell)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, types.SideTypeSell, res)
|
||||||
|
|
||||||
|
res, err = toGlobalSideType("GG")
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestToGlobalOrderType(t *testing.T) {
|
||||||
|
res, err := toGlobalOrderType(bybitapi.OrderTypeMarket)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, types.OrderTypeMarket, res)
|
||||||
|
|
||||||
|
res, err = toGlobalOrderType(bybitapi.OrderTypeLimit)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, types.OrderTypeLimit, res)
|
||||||
|
|
||||||
|
res, err = toGlobalOrderType("GG")
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestToGlobalTimeInForce(t *testing.T) {
|
||||||
|
res, err := toGlobalTimeInForce(bybitapi.TimeInForceGTC)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, types.TimeInForceGTC, res)
|
||||||
|
|
||||||
|
res, err = toGlobalTimeInForce(bybitapi.TimeInForceIOC)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, types.TimeInForceIOC, res)
|
||||||
|
|
||||||
|
res, err = toGlobalTimeInForce(bybitapi.TimeInForceFOK)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, types.TimeInForceFOK, res)
|
||||||
|
|
||||||
|
res, err = toGlobalTimeInForce("GG")
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestToGlobalOrderStatus(t *testing.T) {
|
||||||
|
t.Run("New", func(t *testing.T) {
|
||||||
|
res, err := toGlobalOrderStatus(bybitapi.OrderStatusNew)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, types.OrderStatusNew, res)
|
||||||
|
|
||||||
|
res, err = toGlobalOrderStatus(bybitapi.OrderStatusActive)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, types.OrderStatusNew, res)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Filled", func(t *testing.T) {
|
||||||
|
res, err := toGlobalOrderStatus(bybitapi.OrderStatusFilled)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, types.OrderStatusFilled, res)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("PartiallyFilled", func(t *testing.T) {
|
||||||
|
res, err := toGlobalOrderStatus(bybitapi.OrderStatusPartiallyFilled)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, types.OrderStatusPartiallyFilled, res)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("OrderStatusCanceled", func(t *testing.T) {
|
||||||
|
res, err := toGlobalOrderStatus(bybitapi.OrderStatusCancelled)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, types.OrderStatusCanceled, res)
|
||||||
|
|
||||||
|
res, err = toGlobalOrderStatus(bybitapi.OrderStatusPartiallyFilledCanceled)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, types.OrderStatusCanceled, res)
|
||||||
|
|
||||||
|
res, err = toGlobalOrderStatus(bybitapi.OrderStatusDeactivated)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, types.OrderStatusCanceled, res)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("OrderStatusRejected", func(t *testing.T) {
|
||||||
|
res, err := toGlobalOrderStatus(bybitapi.OrderStatusRejected)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, types.OrderStatusRejected, res)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsWorking(t *testing.T) {
|
||||||
|
for _, s := range bybitapi.AllOrderStatuses {
|
||||||
|
res, err := isWorking(s)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
if res {
|
||||||
|
gos, err := toGlobalOrderStatus(s)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, gos == types.OrderStatusNew || gos == types.OrderStatusPartiallyFilled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -17,11 +17,14 @@ import (
|
||||||
//
|
//
|
||||||
// The default order limiter apply 2 requests per second and a 2 initial bucket
|
// The default order limiter apply 2 requests per second and a 2 initial bucket
|
||||||
// this includes QueryMarkets, QueryTicker
|
// this includes QueryMarkets, QueryTicker
|
||||||
var sharedRateLimiter = rate.NewLimiter(rate.Every(time.Second/2), 2)
|
var (
|
||||||
|
sharedRateLimiter = rate.NewLimiter(rate.Every(time.Second/2), 2)
|
||||||
|
tradeRateLimiter = rate.NewLimiter(rate.Every(time.Second/5), 5)
|
||||||
|
|
||||||
var log = logrus.WithFields(logrus.Fields{
|
log = logrus.WithFields(logrus.Fields{
|
||||||
"exchange": "bybit",
|
"exchange": "bybit",
|
||||||
})
|
})
|
||||||
|
)
|
||||||
|
|
||||||
type Exchange struct {
|
type Exchange struct {
|
||||||
key, secret string
|
key, secret string
|
||||||
|
@ -128,3 +131,41 @@ func (e *Exchange) QueryTickers(ctx context.Context, symbols ...string) (map[str
|
||||||
|
|
||||||
return tickers, nil
|
return tickers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *Exchange) QueryOpenOrders(ctx context.Context, symbol string) (orders []types.Order, err error) {
|
||||||
|
cursor := ""
|
||||||
|
for {
|
||||||
|
req := e.client.NewGetOpenOrderRequest().Symbol(symbol)
|
||||||
|
if len(cursor) != 0 {
|
||||||
|
// the default limit is 20.
|
||||||
|
req = req.Cursor(cursor)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = tradeRateLimiter.Wait(ctx); err != nil {
|
||||||
|
log.WithError(err).Errorf("trade rate limiter wait error")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
res, err := req.Do(ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("failed to get open order, cursor: %s, err: %v", cursor, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, order := range res.List {
|
||||||
|
order, err := toGlobalOrder(order)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("failed to convert order, err: %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
orders = append(orders, *order)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(res.NextPageCursor) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
cursor = res.NextPageCursor
|
||||||
|
}
|
||||||
|
|
||||||
|
return orders, nil
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user