mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-10 09:11:55 +00:00
Merge pull request #1399 from c9s/edwin/bitget/submit-orders
FEATURE: [bitget] support submit order
This commit is contained in:
commit
58a810ecc9
|
@ -2,11 +2,12 @@ package bitgetapi
|
|||
|
||||
import (
|
||||
"context"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"os"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/exchange/bitget/bitgetapi"
|
||||
"github.com/c9s/bbgo/pkg/testutil"
|
||||
)
|
||||
|
@ -45,4 +46,16 @@ func TestClient(t *testing.T) {
|
|||
|
||||
t.Logf("place order resp: %+v", req)
|
||||
})
|
||||
|
||||
t.Run("PlaceOrderRequest", func(t *testing.T) {
|
||||
req, err := client.NewPlaceOrderRequest().Symbol("APEUSDT").OrderType(OrderTypeLimit).
|
||||
Side(SideTypeSell).
|
||||
Price("2").
|
||||
Size("5").
|
||||
Force(OrderForceGTC).
|
||||
Do(context.Background())
|
||||
assert.NoError(t, err)
|
||||
|
||||
t.Logf("place order resp: %+v", req)
|
||||
})
|
||||
}
|
||||
|
|
29
pkg/exchange/bitget/bitgetapi/v2/place_order_request.go
Normal file
29
pkg/exchange/bitget/bitgetapi/v2/place_order_request.go
Normal file
|
@ -0,0 +1,29 @@
|
|||
package bitgetapi
|
||||
|
||||
//go:generate -command GetRequest requestgen -method GET -responseType .APIResponse -responseDataField Data
|
||||
//go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Data
|
||||
|
||||
import (
|
||||
"github.com/c9s/requestgen"
|
||||
)
|
||||
|
||||
type PlaceOrderResponse struct {
|
||||
OrderId string `json:"orderId"`
|
||||
ClientOrderId string `json:"clientOrderId"`
|
||||
}
|
||||
|
||||
//go:generate PostRequest -url "/api/v2/spot/trade/place-order" -type PlaceOrderRequest -responseDataType .PlaceOrderResponse
|
||||
type PlaceOrderRequest struct {
|
||||
client requestgen.AuthenticatedAPIClient
|
||||
symbol string `param:"symbol"`
|
||||
orderType OrderType `param:"orderType"`
|
||||
side SideType `param:"side"`
|
||||
force OrderForce `param:"force"`
|
||||
price *string `param:"price"`
|
||||
size string `param:"size"`
|
||||
clientOrderId *string `param:"clientOid"`
|
||||
}
|
||||
|
||||
func (c *Client) NewPlaceOrderRequest() *PlaceOrderRequest {
|
||||
return &PlaceOrderRequest{client: c.Client}
|
||||
}
|
|
@ -0,0 +1,251 @@
|
|||
// Code generated by "requestgen -method POST -responseType .APIResponse -responseDataField Data -url /api/v2/spot/trade/place-order -type PlaceOrderRequest -responseDataType .PlaceOrderResponse"; DO NOT EDIT.
|
||||
|
||||
package bitgetapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/c9s/bbgo/pkg/exchange/bitget/bitgetapi"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
func (p *PlaceOrderRequest) Symbol(symbol string) *PlaceOrderRequest {
|
||||
p.symbol = symbol
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *PlaceOrderRequest) OrderType(orderType OrderType) *PlaceOrderRequest {
|
||||
p.orderType = orderType
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *PlaceOrderRequest) Side(side SideType) *PlaceOrderRequest {
|
||||
p.side = side
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *PlaceOrderRequest) Force(force OrderForce) *PlaceOrderRequest {
|
||||
p.force = force
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *PlaceOrderRequest) Price(price string) *PlaceOrderRequest {
|
||||
p.price = &price
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *PlaceOrderRequest) Size(size string) *PlaceOrderRequest {
|
||||
p.size = size
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *PlaceOrderRequest) ClientOrderId(clientOrderId string) *PlaceOrderRequest {
|
||||
p.clientOrderId = &clientOrderId
|
||||
return p
|
||||
}
|
||||
|
||||
// GetQueryParameters builds and checks the query parameters and returns url.Values
|
||||
func (p *PlaceOrderRequest) 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 (p *PlaceOrderRequest) GetParameters() (map[string]interface{}, error) {
|
||||
var params = map[string]interface{}{}
|
||||
// check symbol field -> json key symbol
|
||||
symbol := p.symbol
|
||||
|
||||
// assign parameter of symbol
|
||||
params["symbol"] = symbol
|
||||
// check orderType field -> json key orderType
|
||||
orderType := p.orderType
|
||||
|
||||
// TEMPLATE check-valid-values
|
||||
switch orderType {
|
||||
case OrderTypeLimit, OrderTypeMarket:
|
||||
params["orderType"] = orderType
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("orderType value %v is invalid", orderType)
|
||||
|
||||
}
|
||||
// END TEMPLATE check-valid-values
|
||||
|
||||
// assign parameter of orderType
|
||||
params["orderType"] = orderType
|
||||
// check side field -> json key side
|
||||
side := p.side
|
||||
|
||||
// TEMPLATE check-valid-values
|
||||
switch side {
|
||||
case SideTypeBuy, SideTypeSell:
|
||||
params["side"] = side
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("side value %v is invalid", side)
|
||||
|
||||
}
|
||||
// END TEMPLATE check-valid-values
|
||||
|
||||
// assign parameter of side
|
||||
params["side"] = side
|
||||
// check force field -> json key force
|
||||
force := p.force
|
||||
|
||||
// TEMPLATE check-valid-values
|
||||
switch force {
|
||||
case OrderForceGTC, OrderForcePostOnly, OrderForceFOK, OrderForceIOC:
|
||||
params["force"] = force
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("force value %v is invalid", force)
|
||||
|
||||
}
|
||||
// END TEMPLATE check-valid-values
|
||||
|
||||
// assign parameter of force
|
||||
params["force"] = force
|
||||
// check price field -> json key price
|
||||
if p.price != nil {
|
||||
price := *p.price
|
||||
|
||||
// assign parameter of price
|
||||
params["price"] = price
|
||||
} else {
|
||||
}
|
||||
// check size field -> json key size
|
||||
size := p.size
|
||||
|
||||
// assign parameter of size
|
||||
params["size"] = size
|
||||
// check clientOrderId field -> json key clientOid
|
||||
if p.clientOrderId != nil {
|
||||
clientOrderId := *p.clientOrderId
|
||||
|
||||
// assign parameter of clientOrderId
|
||||
params["clientOid"] = clientOrderId
|
||||
} else {
|
||||
}
|
||||
|
||||
return params, nil
|
||||
}
|
||||
|
||||
// GetParametersQuery converts the parameters from GetParameters into the url.Values format
|
||||
func (p *PlaceOrderRequest) GetParametersQuery() (url.Values, error) {
|
||||
query := url.Values{}
|
||||
|
||||
params, err := p.GetParameters()
|
||||
if err != nil {
|
||||
return query, err
|
||||
}
|
||||
|
||||
for _k, _v := range params {
|
||||
if p.isVarSlice(_v) {
|
||||
p.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 (p *PlaceOrderRequest) GetParametersJSON() ([]byte, error) {
|
||||
params, err := p.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 (p *PlaceOrderRequest) GetSlugParameters() (map[string]interface{}, error) {
|
||||
var params = map[string]interface{}{}
|
||||
|
||||
return params, nil
|
||||
}
|
||||
|
||||
func (p *PlaceOrderRequest) 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 (p *PlaceOrderRequest) 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 (p *PlaceOrderRequest) isVarSlice(_v interface{}) bool {
|
||||
rt := reflect.TypeOf(_v)
|
||||
switch rt.Kind() {
|
||||
case reflect.Slice:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *PlaceOrderRequest) GetSlugsMap() (map[string]string, error) {
|
||||
slugs := map[string]string{}
|
||||
params, err := p.GetSlugParameters()
|
||||
if err != nil {
|
||||
return slugs, nil
|
||||
}
|
||||
|
||||
for _k, _v := range params {
|
||||
slugs[_k] = fmt.Sprintf("%v", _v)
|
||||
}
|
||||
|
||||
return slugs, nil
|
||||
}
|
||||
|
||||
func (p *PlaceOrderRequest) Do(ctx context.Context) (*PlaceOrderResponse, error) {
|
||||
|
||||
params, err := p.GetParameters()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
query := url.Values{}
|
||||
|
||||
apiURL := "/api/v2/spot/trade/place-order"
|
||||
|
||||
req, err := p.client.NewAuthenticatedRequest(ctx, "POST", apiURL, query, params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response, err := p.client.SendRequest(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var apiResponse bitgetapi.APIResponse
|
||||
if err := response.DecodeJSON(&apiResponse); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var data PlaceOrderResponse
|
||||
if err := json.Unmarshal(apiResponse.Data, &data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &data, nil
|
||||
}
|
|
@ -243,3 +243,29 @@ func processMarketBuyQuantity(filledQty, filledPrice, priceAvg, buyQty fixedpoin
|
|||
return fixedpoint.Zero, fmt.Errorf("failed to execute market buy quantity due to unexpected order status %s ", orderStatus)
|
||||
}
|
||||
}
|
||||
|
||||
func toLocalOrderType(orderType types.OrderType) (v2.OrderType, error) {
|
||||
switch orderType {
|
||||
case types.OrderTypeLimit:
|
||||
return v2.OrderTypeLimit, nil
|
||||
|
||||
case types.OrderTypeMarket:
|
||||
return v2.OrderTypeMarket, nil
|
||||
|
||||
default:
|
||||
return "", fmt.Errorf("order type %s not supported", orderType)
|
||||
}
|
||||
}
|
||||
|
||||
func toLocalSide(side types.SideType) (v2.SideType, error) {
|
||||
switch side {
|
||||
case types.SideTypeSell:
|
||||
return v2.SideTypeSell, nil
|
||||
|
||||
case types.SideTypeBuy:
|
||||
return v2.SideTypeBuy, nil
|
||||
|
||||
default:
|
||||
return "", fmt.Errorf("side type %s not supported", side)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -465,3 +465,29 @@ func Test_processMarketBuyQuantity(t *testing.T) {
|
|||
assert.ErrorContains(err, "xxx")
|
||||
})
|
||||
}
|
||||
|
||||
func Test_toLocalOrderType(t *testing.T) {
|
||||
orderType, err := toLocalOrderType(types.OrderTypeLimit)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, v2.OrderTypeLimit, orderType)
|
||||
|
||||
orderType, err = toLocalOrderType(types.OrderTypeMarket)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, v2.OrderTypeMarket, orderType)
|
||||
|
||||
_, err = toLocalOrderType("xxx")
|
||||
assert.ErrorContains(t, err, "xxx")
|
||||
}
|
||||
|
||||
func Test_toLocalSide(t *testing.T) {
|
||||
orderType, err := toLocalSide(types.SideTypeSell)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, v2.SideTypeSell, orderType)
|
||||
|
||||
orderType, err = toLocalSide(types.SideTypeBuy)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, v2.SideTypeBuy, orderType)
|
||||
|
||||
_, err = toLocalOrderType("xxx")
|
||||
assert.ErrorContains(t, err, "xxx")
|
||||
}
|
||||
|
|
|
@ -42,6 +42,8 @@ var (
|
|||
queryOpenOrdersRateLimiter = rate.NewLimiter(rate.Every(time.Second/10), 5)
|
||||
// closedQueryOrdersRateLimiter has its own rate limit. https://www.bitget.com/api-doc/spot/trade/Get-History-Orders
|
||||
closedQueryOrdersRateLimiter = rate.NewLimiter(rate.Every(time.Second/15), 5)
|
||||
// submitOrdersRateLimiter has its own rate limit. https://www.bitget.com/zh-CN/api-doc/spot/trade/Place-Order
|
||||
submitOrdersRateLimiter = rate.NewLimiter(rate.Every(time.Second/5), 5)
|
||||
)
|
||||
|
||||
type Exchange struct {
|
||||
|
@ -183,9 +185,114 @@ func (e *Exchange) QueryAccountBalances(ctx context.Context) (types.BalanceMap,
|
|||
return bals, nil
|
||||
}
|
||||
|
||||
// SubmitOrder submits an order.
|
||||
//
|
||||
// Remark:
|
||||
// 1. We support only GTC for time-in-force, because the response from queryOrder does not include time-in-force information.
|
||||
// 2. For market buy orders, the size unit is quote currency, whereas the unit for order.Quantity is in base currency.
|
||||
// Therefore, we need to calculate the equivalent quote currency amount based on the ticker data.
|
||||
//
|
||||
// Note that there is a bug in Bitget where you can place a market order with the 'post_only' option successfully,
|
||||
// which should not be possible. The issue has been reported.
|
||||
func (e *Exchange) SubmitOrder(ctx context.Context, order types.SubmitOrder) (createdOrder *types.Order, err error) {
|
||||
// TODO implement me
|
||||
panic("implement me")
|
||||
if len(order.Market.Symbol) == 0 {
|
||||
return nil, fmt.Errorf("order.Market.Symbol is required: %+v", order)
|
||||
}
|
||||
|
||||
req := e.v2Client.NewPlaceOrderRequest()
|
||||
req.Symbol(order.Market.Symbol)
|
||||
|
||||
// set order type
|
||||
orderType, err := toLocalOrderType(order.Type)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.OrderType(orderType)
|
||||
|
||||
// set side
|
||||
side, err := toLocalSide(order.Side)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Side(side)
|
||||
|
||||
// set quantity
|
||||
qty := order.Quantity
|
||||
// if the order is market buy, the quantity is quote coin, instead of base coin. so we need to convert it.
|
||||
if order.Type == types.OrderTypeMarket && order.Side == types.SideTypeBuy {
|
||||
ticker, err := e.QueryTicker(ctx, order.Market.Symbol)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
qty = order.Quantity.Mul(ticker.Buy)
|
||||
}
|
||||
req.Size(order.Market.FormatQuantity(qty))
|
||||
|
||||
// we support only GTC/PostOnly, this is because:
|
||||
// 1. We support only SPOT trading.
|
||||
// 2. The query oepn/closed order does not including the `force` in SPOT.
|
||||
// If we support FOK/IOC, but you can't query them, that would be unreasonable.
|
||||
// The other case to consider is 'PostOnly', which is a trade-off because we want to support 'xmaker'.
|
||||
if order.TimeInForce != types.TimeInForceGTC {
|
||||
return nil, fmt.Errorf("time-in-force %s not supported", order.TimeInForce)
|
||||
}
|
||||
req.Force(v2.OrderForceGTC)
|
||||
// set price
|
||||
if order.Type == types.OrderTypeLimit || order.Type == types.OrderTypeLimitMaker {
|
||||
req.Price(order.Market.FormatPrice(order.Price))
|
||||
|
||||
if order.Type == types.OrderTypeLimitMaker {
|
||||
req.Force(v2.OrderForcePostOnly)
|
||||
}
|
||||
}
|
||||
|
||||
// set client order id
|
||||
if len(order.ClientOrderID) > maxOrderIdLen {
|
||||
return nil, fmt.Errorf("unexpected length of order id, got: %d", len(order.ClientOrderID))
|
||||
}
|
||||
if len(order.ClientOrderID) > 0 {
|
||||
req.ClientOrderId(order.ClientOrderID)
|
||||
}
|
||||
|
||||
if err := submitOrdersRateLimiter.Wait(ctx); err != nil {
|
||||
return nil, fmt.Errorf("place order rate limiter wait error: %w", err)
|
||||
}
|
||||
res, err := req.Do(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to place order, order: %#v, err: %w", order, err)
|
||||
}
|
||||
|
||||
if len(res.OrderId) == 0 || (len(order.ClientOrderID) != 0 && res.ClientOrderId != order.ClientOrderID) {
|
||||
return nil, fmt.Errorf("unexpected order id, resp: %#v, order: %#v", res, order)
|
||||
}
|
||||
|
||||
orderId := res.OrderId
|
||||
ordersResp, err := e.v2Client.NewGetUnfilledOrdersRequest().OrderId(orderId).Do(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to query open order by order id: %s, err: %w", orderId, err)
|
||||
}
|
||||
|
||||
switch len(ordersResp) {
|
||||
case 0:
|
||||
// The market order will be executed immediately, so we cannot retrieve it through the NewGetUnfilledOrdersRequest API.
|
||||
// Try to get the order from the NewGetHistoryOrdersRequest API.
|
||||
ordersResp, err := e.v2Client.NewGetHistoryOrdersRequest().OrderId(orderId).Do(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to query history order by order id: %s, err: %w", orderId, err)
|
||||
}
|
||||
|
||||
if len(ordersResp) != 1 {
|
||||
return nil, fmt.Errorf("unexpected order length, order id: %s", orderId)
|
||||
}
|
||||
|
||||
return toGlobalOrder(ordersResp[0])
|
||||
|
||||
case 1:
|
||||
return unfilledOrderToGlobalOrder(ordersResp[0])
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected order length, order id: %s", orderId)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Exchange) QueryOpenOrders(ctx context.Context, symbol string) (orders []types.Order, err error) {
|
||||
|
@ -238,7 +345,7 @@ func (e *Exchange) QueryOpenOrders(ctx context.Context, symbol string) (orders [
|
|||
// ** Since and Until cannot exceed 90 days. **
|
||||
// ** Since from the last 90 days can be queried. **
|
||||
func (e *Exchange) QueryClosedOrders(ctx context.Context, symbol string, since, until time.Time, lastOrderID uint64) (orders []types.Order, err error) {
|
||||
if since.Sub(time.Now()) > queryMaxDuration {
|
||||
if time.Since(since) > queryMaxDuration {
|
||||
return nil, fmt.Errorf("start time from the last 90 days can be queried, got: %s", since)
|
||||
}
|
||||
if until.Before(since) {
|
||||
|
|
Loading…
Reference in New Issue
Block a user