pkg/exchange: add QueryOpenOrders API for bybit

This commit is contained in:
Edwin 2023-07-25 21:30:49 +08:00
parent ff78637c8f
commit 6d4deb54cc
8 changed files with 870 additions and 7 deletions

View File

@ -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) {

View File

@ -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
}
})
} }

View 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,
}
}

View File

@ -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
}

View File

@ -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"
)

View File

@ -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
}

View File

@ -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)
}
}
}

View File

@ -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
}