mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-26 00:35:15 +00:00
Merge pull request #1394 from c9s/edwin/bitget/ClosedOpenOrders
FEATURE: [bitget] support query closed orders
This commit is contained in:
commit
66caf78556
|
@ -2,12 +2,11 @@ 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"
|
||||
)
|
||||
|
@ -25,7 +24,6 @@ func getTestClientOrSkip(t *testing.T) *Client {
|
|||
|
||||
client := bitgetapi.NewClient()
|
||||
client.Auth(key, secret, os.Getenv("BITGET_API_PASSPHRASE"))
|
||||
|
||||
return NewClient(client)
|
||||
}
|
||||
|
||||
|
@ -39,4 +37,12 @@ func TestClient(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
t.Logf("resp: %+v", resp)
|
||||
})
|
||||
|
||||
t.Run("GetHistoryOrdersRequest", func(t *testing.T) {
|
||||
// market buy
|
||||
req, err := client.NewGetHistoryOrdersRequest().Symbol("APEUSDT").Do(ctx)
|
||||
assert.NoError(t, err)
|
||||
|
||||
t.Logf("place order resp: %+v", req)
|
||||
})
|
||||
}
|
||||
|
|
102
pkg/exchange/bitget/bitgetapi/v2/get_history_orders_request.go
Normal file
102
pkg/exchange/bitget/bitgetapi/v2/get_history_orders_request.go
Normal file
|
@ -0,0 +1,102 @@
|
|||
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 (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
"github.com/c9s/requestgen"
|
||||
)
|
||||
|
||||
type FeeDetail struct {
|
||||
// NewFees should have a value because when I was integrating, it already prompted,
|
||||
// "If there is no 'newFees' field, this data represents earlier historical data."
|
||||
NewFees struct {
|
||||
// Amount deducted by coupons, unit:currency obtained from the transaction.
|
||||
DeductedByCoupon fixedpoint.Value `json:"c"`
|
||||
// Amount deducted in BGB (Bitget Coin), unit:BGB
|
||||
DeductedInBGB fixedpoint.Value `json:"d"`
|
||||
// If the BGB balance is insufficient to cover the fees, the remaining amount is deducted from the
|
||||
//currency obtained from the transaction.
|
||||
DeductedFromCurrency fixedpoint.Value `json:"r"`
|
||||
// The total fee amount to be paid, unit :currency obtained from the transaction.
|
||||
ToBePaid fixedpoint.Value `json:"t"`
|
||||
// ignored
|
||||
Deduction bool `json:"deduction"`
|
||||
// ignored
|
||||
TotalDeductionFee fixedpoint.Value `json:"totalDeductionFee"`
|
||||
} `json:"newFees"`
|
||||
}
|
||||
|
||||
type OrderDetail struct {
|
||||
UserId types.StrInt64 `json:"userId"`
|
||||
Symbol string `json:"symbol"`
|
||||
// OrderId are always numeric. It's confirmed with official customer service. https://t.me/bitgetOpenapi/24172
|
||||
OrderId types.StrInt64 `json:"orderId"`
|
||||
ClientOrderId string `json:"clientOid"`
|
||||
Price fixedpoint.Value `json:"price"`
|
||||
// Size is base coin when orderType=limit; quote coin when orderType=market
|
||||
Size fixedpoint.Value `json:"size"`
|
||||
OrderType OrderType `json:"orderType"`
|
||||
Side SideType `json:"side"`
|
||||
Status OrderStatus `json:"status"`
|
||||
PriceAvg fixedpoint.Value `json:"priceAvg"`
|
||||
BaseVolume fixedpoint.Value `json:"baseVolume"`
|
||||
QuoteVolume fixedpoint.Value `json:"quoteVolume"`
|
||||
EnterPointSource string `json:"enterPointSource"`
|
||||
// The value is json string, so we unmarshal it after unmarshal OrderDetail
|
||||
FeeDetailRaw string `json:"feeDetail"`
|
||||
OrderSource string `json:"orderSource"`
|
||||
CTime types.MillisecondTimestamp `json:"cTime"`
|
||||
UTime types.MillisecondTimestamp `json:"uTime"`
|
||||
|
||||
FeeDetail FeeDetail
|
||||
}
|
||||
|
||||
func (o *OrderDetail) UnmarshalJSON(data []byte) error {
|
||||
if o == nil {
|
||||
return fmt.Errorf("failed to unmarshal json from nil pointer order detail")
|
||||
}
|
||||
// define new type to avoid loop reference
|
||||
type AuxOrderDetail OrderDetail
|
||||
|
||||
var aux AuxOrderDetail
|
||||
if err := json.Unmarshal(data, &aux); err != nil {
|
||||
return err
|
||||
}
|
||||
*o = OrderDetail(aux)
|
||||
|
||||
if len(aux.FeeDetailRaw) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var feeDetail FeeDetail
|
||||
if err := json.Unmarshal([]byte(aux.FeeDetailRaw), &feeDetail); err != nil {
|
||||
return fmt.Errorf("unexpected fee detail raw: %s, err: %w", aux.FeeDetailRaw, err)
|
||||
}
|
||||
o.FeeDetail = feeDetail
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
//go:generate GetRequest -url "/api/v2/spot/trade/history-orders" -type GetHistoryOrdersRequest -responseDataType []OrderDetail
|
||||
type GetHistoryOrdersRequest struct {
|
||||
client requestgen.AuthenticatedAPIClient
|
||||
|
||||
symbol *string `param:"symbol,query"`
|
||||
// Limit number default 100 max 100
|
||||
limit *string `param:"limit,query"`
|
||||
// idLessThan requests the content on the page before this ID (older data), the value input should be the orderId of the corresponding interface.
|
||||
idLessThan *string `param:"idLessThan,query"`
|
||||
startTime *int64 `param:"startTime,query"`
|
||||
endTime *int64 `param:"endTime,query"`
|
||||
orderId *string `param:"orderId,query"`
|
||||
}
|
||||
|
||||
func (c *Client) NewGetHistoryOrdersRequest() *GetHistoryOrdersRequest {
|
||||
return &GetHistoryOrdersRequest{client: c.Client}
|
||||
}
|
|
@ -0,0 +1,222 @@
|
|||
// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Data -url /api/v2/spot/trade/history-orders -type GetHistoryOrdersRequest -responseDataType []OrderDetail"; DO NOT EDIT.
|
||||
|
||||
package bitgetapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/c9s/bbgo/pkg/exchange/bitget/bitgetapi"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
func (g *GetHistoryOrdersRequest) Symbol(symbol string) *GetHistoryOrdersRequest {
|
||||
g.symbol = &symbol
|
||||
return g
|
||||
}
|
||||
|
||||
func (g *GetHistoryOrdersRequest) Limit(limit string) *GetHistoryOrdersRequest {
|
||||
g.limit = &limit
|
||||
return g
|
||||
}
|
||||
|
||||
func (g *GetHistoryOrdersRequest) IdLessThan(idLessThan string) *GetHistoryOrdersRequest {
|
||||
g.idLessThan = &idLessThan
|
||||
return g
|
||||
}
|
||||
|
||||
func (g *GetHistoryOrdersRequest) StartTime(startTime int64) *GetHistoryOrdersRequest {
|
||||
g.startTime = &startTime
|
||||
return g
|
||||
}
|
||||
|
||||
func (g *GetHistoryOrdersRequest) EndTime(endTime int64) *GetHistoryOrdersRequest {
|
||||
g.endTime = &endTime
|
||||
return g
|
||||
}
|
||||
|
||||
func (g *GetHistoryOrdersRequest) OrderId(orderId string) *GetHistoryOrdersRequest {
|
||||
g.orderId = &orderId
|
||||
return g
|
||||
}
|
||||
|
||||
// GetQueryParameters builds and checks the query parameters and returns url.Values
|
||||
func (g *GetHistoryOrdersRequest) GetQueryParameters() (url.Values, error) {
|
||||
var params = map[string]interface{}{}
|
||||
// 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 idLessThan field -> json key idLessThan
|
||||
if g.idLessThan != nil {
|
||||
idLessThan := *g.idLessThan
|
||||
|
||||
// assign parameter of idLessThan
|
||||
params["idLessThan"] = idLessThan
|
||||
} else {
|
||||
}
|
||||
// check startTime field -> json key startTime
|
||||
if g.startTime != nil {
|
||||
startTime := *g.startTime
|
||||
|
||||
// assign parameter of startTime
|
||||
params["startTime"] = startTime
|
||||
} else {
|
||||
}
|
||||
// check endTime field -> json key endTime
|
||||
if g.endTime != nil {
|
||||
endTime := *g.endTime
|
||||
|
||||
// assign parameter of endTime
|
||||
params["endTime"] = endTime
|
||||
} else {
|
||||
}
|
||||
// check orderId field -> json key orderId
|
||||
if g.orderId != nil {
|
||||
orderId := *g.orderId
|
||||
|
||||
// assign parameter of orderId
|
||||
params["orderId"] = orderId
|
||||
} 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 *GetHistoryOrdersRequest) 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 *GetHistoryOrdersRequest) 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 *GetHistoryOrdersRequest) 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 *GetHistoryOrdersRequest) GetSlugParameters() (map[string]interface{}, error) {
|
||||
var params = map[string]interface{}{}
|
||||
|
||||
return params, nil
|
||||
}
|
||||
|
||||
func (g *GetHistoryOrdersRequest) 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 *GetHistoryOrdersRequest) 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 *GetHistoryOrdersRequest) isVarSlice(_v interface{}) bool {
|
||||
rt := reflect.TypeOf(_v)
|
||||
switch rt.Kind() {
|
||||
case reflect.Slice:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (g *GetHistoryOrdersRequest) 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 *GetHistoryOrdersRequest) Do(ctx context.Context) ([]OrderDetail, error) {
|
||||
|
||||
// no body params
|
||||
var params interface{}
|
||||
query, err := g.GetQueryParameters()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
apiURL := "/api/v2/spot/trade/history-orders"
|
||||
|
||||
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 bitgetapi.APIResponse
|
||||
if err := response.DecodeJSON(&apiResponse); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var data []OrderDetail
|
||||
if err := json.Unmarshal(apiResponse.Data, &data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return data, nil
|
||||
}
|
|
@ -0,0 +1,121 @@
|
|||
package bitgetapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
)
|
||||
|
||||
func TestOrderDetail_UnmarshalJSON(t *testing.T) {
|
||||
var (
|
||||
assert = assert.New(t)
|
||||
)
|
||||
t.Run("empty fee", func(t *testing.T) {
|
||||
input := `{
|
||||
"userId":"8672173294",
|
||||
"symbol":"APEUSDT",
|
||||
"orderId":"1104342023170068480",
|
||||
"clientOid":"f3d6a1ee-4e94-48b5-a6e0-25f3e93d92e1",
|
||||
"price":"1.2000000000000000",
|
||||
"size":"5.0000000000000000",
|
||||
"orderType":"limit",
|
||||
"side":"buy",
|
||||
"status":"cancelled",
|
||||
"priceAvg":"0",
|
||||
"baseVolume":"0.0000000000000000",
|
||||
"quoteVolume":"0.0000000000000000",
|
||||
"enterPointSource":"API",
|
||||
"feeDetail":"",
|
||||
"orderSource":"normal",
|
||||
"cTime":"1699021576683",
|
||||
"uTime":"1699021649099"
|
||||
}`
|
||||
var od OrderDetail
|
||||
err := json.Unmarshal([]byte(input), &od)
|
||||
assert.NoError(err)
|
||||
assert.Equal(OrderDetail{
|
||||
UserId: types.StrInt64(8672173294),
|
||||
Symbol: "APEUSDT",
|
||||
OrderId: types.StrInt64(1104342023170068480),
|
||||
ClientOrderId: "f3d6a1ee-4e94-48b5-a6e0-25f3e93d92e1",
|
||||
Price: fixedpoint.NewFromFloat(1.2),
|
||||
Size: fixedpoint.NewFromFloat(5),
|
||||
OrderType: OrderTypeLimit,
|
||||
Side: SideTypeBuy,
|
||||
Status: OrderStatusCancelled,
|
||||
PriceAvg: fixedpoint.Zero,
|
||||
BaseVolume: fixedpoint.Zero,
|
||||
QuoteVolume: fixedpoint.Zero,
|
||||
EnterPointSource: "API",
|
||||
FeeDetailRaw: "",
|
||||
OrderSource: "normal",
|
||||
CTime: types.NewMillisecondTimestampFromInt(1699021576683),
|
||||
UTime: types.NewMillisecondTimestampFromInt(1699021649099),
|
||||
FeeDetail: FeeDetail{},
|
||||
}, od)
|
||||
})
|
||||
|
||||
t.Run("fee", func(t *testing.T) {
|
||||
input := `{
|
||||
"userId":"8672173294",
|
||||
"symbol":"APEUSDT",
|
||||
"orderId":"1104337778433757184",
|
||||
"clientOid":"8afea7bd-d873-44fe-aff8-6a1fae3cc765",
|
||||
"price":"1.4000000000000000",
|
||||
"size":"5.0000000000000000",
|
||||
"orderType":"limit",
|
||||
"side":"sell",
|
||||
"status":"filled",
|
||||
"priceAvg":"1.4001000000000000",
|
||||
"baseVolume":"5.0000000000000000",
|
||||
"quoteVolume":"7.0005000000000000",
|
||||
"enterPointSource":"API",
|
||||
"feeDetail":"{\"newFees\":{\"c\":0,\"d\":0,\"deduction\":false,\"r\":-0.0070005,\"t\":-0.0070005,\"totalDeductionFee\":0},\"USDT\":{\"deduction\":false,\"feeCoinCode\":\"USDT\",\"totalDeductionFee\":0,\"totalFee\":-0.007000500000}}",
|
||||
"orderSource":"normal",
|
||||
"cTime":"1699020564659",
|
||||
"uTime":"1699020564688"
|
||||
}`
|
||||
var od OrderDetail
|
||||
err := json.Unmarshal([]byte(input), &od)
|
||||
assert.NoError(err)
|
||||
assert.Equal(OrderDetail{
|
||||
UserId: types.StrInt64(8672173294),
|
||||
Symbol: "APEUSDT",
|
||||
OrderId: types.StrInt64(1104337778433757184),
|
||||
ClientOrderId: "8afea7bd-d873-44fe-aff8-6a1fae3cc765",
|
||||
Price: fixedpoint.NewFromFloat(1.4),
|
||||
Size: fixedpoint.NewFromFloat(5),
|
||||
OrderType: OrderTypeLimit,
|
||||
Side: SideTypeSell,
|
||||
Status: OrderStatusFilled,
|
||||
PriceAvg: fixedpoint.NewFromFloat(1.4001),
|
||||
BaseVolume: fixedpoint.NewFromFloat(5),
|
||||
QuoteVolume: fixedpoint.NewFromFloat(7.0005),
|
||||
EnterPointSource: "API",
|
||||
FeeDetailRaw: `{"newFees":{"c":0,"d":0,"deduction":false,"r":-0.0070005,"t":-0.0070005,"totalDeductionFee":0},"USDT":{"deduction":false,"feeCoinCode":"USDT","totalDeductionFee":0,"totalFee":-0.007000500000}}`,
|
||||
OrderSource: "normal",
|
||||
CTime: types.NewMillisecondTimestampFromInt(1699020564659),
|
||||
UTime: types.NewMillisecondTimestampFromInt(1699020564688),
|
||||
FeeDetail: FeeDetail{
|
||||
NewFees: struct {
|
||||
DeductedByCoupon fixedpoint.Value `json:"c"`
|
||||
DeductedInBGB fixedpoint.Value `json:"d"`
|
||||
DeductedFromCurrency fixedpoint.Value `json:"r"`
|
||||
ToBePaid fixedpoint.Value `json:"t"`
|
||||
Deduction bool `json:"deduction"`
|
||||
TotalDeductionFee fixedpoint.Value `json:"totalDeductionFee"`
|
||||
}{DeductedByCoupon: fixedpoint.NewFromFloat(0),
|
||||
DeductedInBGB: fixedpoint.NewFromFloat(0),
|
||||
DeductedFromCurrency: fixedpoint.NewFromFloat(-0.0070005),
|
||||
ToBePaid: fixedpoint.NewFromFloat(-0.0070005),
|
||||
Deduction: false,
|
||||
TotalDeductionFee: fixedpoint.Zero,
|
||||
},
|
||||
},
|
||||
}, od)
|
||||
})
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
package bitget
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
|
@ -158,3 +159,87 @@ func unfilledOrderToGlobalOrder(order v2.UnfilledOrder) (*types.Order, error) {
|
|||
UpdateTime: types.Time(order.UTime.Time()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toGlobalOrder(order v2.OrderDetail) (*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
|
||||
}
|
||||
|
||||
status, err := toGlobalOrderStatus(order.Status)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
qty := order.Size
|
||||
price := order.Price
|
||||
|
||||
if orderType == types.OrderTypeMarket {
|
||||
price = order.PriceAvg
|
||||
if side == types.SideTypeBuy {
|
||||
qty, err = processMarketBuyQuantity(order.BaseVolume, order.QuoteVolume, order.PriceAvg, order.Size, order.Status)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &types.Order{
|
||||
SubmitOrder: types.SubmitOrder{
|
||||
ClientOrderID: order.ClientOrderId,
|
||||
Symbol: order.Symbol,
|
||||
Side: side,
|
||||
Type: orderType,
|
||||
Quantity: qty,
|
||||
Price: price,
|
||||
// Bitget does not include the "time-in-force" field in its API response for spot trading, so we set GTC.
|
||||
TimeInForce: types.TimeInForceGTC,
|
||||
},
|
||||
Exchange: types.ExchangeBitget,
|
||||
OrderID: uint64(order.OrderId),
|
||||
UUID: strconv.FormatInt(int64(order.OrderId), 10),
|
||||
Status: status,
|
||||
ExecutedQuantity: order.BaseVolume,
|
||||
IsWorking: order.Status.IsWorking(),
|
||||
CreationTime: types.Time(order.CTime.Time()),
|
||||
UpdateTime: types.Time(order.UTime.Time()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// processMarketBuyQuantity returns the estimated base quantity or real. The order size will be 'quote quantity' when side is buy and
|
||||
// type is market, so we need to convert that. This is because the unit of types.Order.Quantity is base coin.
|
||||
//
|
||||
// If the order status is PartialFilled, return estimated base coin quantity.
|
||||
// If the order status is Filled, return the filled base quantity instead of the buy quantity, because a market order on the buy side
|
||||
// cannot execute all.
|
||||
// Otherwise, return zero.
|
||||
func processMarketBuyQuantity(filledQty, filledPrice, priceAvg, buyQty fixedpoint.Value, orderStatus v2.OrderStatus) (fixedpoint.Value, error) {
|
||||
switch orderStatus {
|
||||
case v2.OrderStatusInit, v2.OrderStatusNew, v2.OrderStatusLive, v2.OrderStatusCancelled:
|
||||
return fixedpoint.Zero, nil
|
||||
|
||||
case v2.OrderStatusPartialFilled:
|
||||
// sanity check for avoid divide 0
|
||||
if priceAvg.IsZero() {
|
||||
return fixedpoint.Zero, errors.New("priceAvg for a partialFilled should not be zero")
|
||||
}
|
||||
// calculate the remaining quote coin quantity.
|
||||
remainPrice := buyQty.Sub(filledPrice)
|
||||
// calculate the remaining base coin quantity.
|
||||
remainBaseCoinQty := remainPrice.Div(priceAvg)
|
||||
// Estimated quantity that may be purchased.
|
||||
return filledQty.Add(remainBaseCoinQty), nil
|
||||
|
||||
case v2.OrderStatusFilled:
|
||||
// Market buy orders may not purchase the entire quantity, hence the use of filledQty here.
|
||||
return filledQty, nil
|
||||
|
||||
default:
|
||||
return fixedpoint.Zero, fmt.Errorf("failed to execute market buy quantity due to unexpected order status %s ", orderStatus)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -272,3 +272,196 @@ func Test_unfilledOrderToGlobalOrder(t *testing.T) {
|
|||
assert.ErrorContains(err, "xxx")
|
||||
})
|
||||
}
|
||||
|
||||
func Test_toGlobalOrder(t *testing.T) {
|
||||
var (
|
||||
assert = assert.New(t)
|
||||
orderId = 1105087175647989764
|
||||
unfilledOrder = v2.OrderDetail{
|
||||
UserId: 123456,
|
||||
Symbol: "BTCUSDT",
|
||||
OrderId: types.StrInt64(orderId),
|
||||
ClientOrderId: "74b86af3-6098-479c-acac-bfb074c067f3",
|
||||
Price: fixedpoint.NewFromFloat(1.2),
|
||||
Size: fixedpoint.NewFromFloat(5),
|
||||
OrderType: v2.OrderTypeLimit,
|
||||
Side: v2.SideTypeBuy,
|
||||
Status: v2.OrderStatusFilled,
|
||||
PriceAvg: fixedpoint.NewFromFloat(1.4),
|
||||
BaseVolume: fixedpoint.NewFromFloat(5),
|
||||
QuoteVolume: fixedpoint.NewFromFloat(7.0005),
|
||||
EnterPointSource: "API",
|
||||
FeeDetailRaw: `{\"newFees\":{\"c\":0,\"d\":0,\"deduction\":false,\"r\":-0.0070005,\"t\":-0.0070005,\"totalDeductionFee\":0},\"USDT\":{\"deduction\":false,\"feeCoinCode\":\"USDT\",\"totalDeductionFee\":0,\"totalFee\":-0.007000500000}}`,
|
||||
OrderSource: "normal",
|
||||
CTime: types.NewMillisecondTimestampFromInt(1660704288118),
|
||||
UTime: types.NewMillisecondTimestampFromInt(1660704288118),
|
||||
}
|
||||
|
||||
expOrder = &types.Order{
|
||||
SubmitOrder: types.SubmitOrder{
|
||||
ClientOrderID: "74b86af3-6098-479c-acac-bfb074c067f3",
|
||||
Symbol: "BTCUSDT",
|
||||
Side: types.SideTypeBuy,
|
||||
Type: types.OrderTypeLimit,
|
||||
Quantity: fixedpoint.NewFromFloat(5),
|
||||
Price: fixedpoint.NewFromFloat(1.2),
|
||||
TimeInForce: types.TimeInForceGTC,
|
||||
},
|
||||
Exchange: types.ExchangeBitget,
|
||||
OrderID: uint64(orderId),
|
||||
UUID: strconv.FormatInt(int64(orderId), 10),
|
||||
Status: types.OrderStatusFilled,
|
||||
ExecutedQuantity: fixedpoint.NewFromFloat(5),
|
||||
IsWorking: false,
|
||||
CreationTime: types.Time(types.NewMillisecondTimestampFromInt(1660704288118).Time()),
|
||||
UpdateTime: types.Time(types.NewMillisecondTimestampFromInt(1660704288118).Time()),
|
||||
}
|
||||
)
|
||||
|
||||
t.Run("succeeds with limit buy", func(t *testing.T) {
|
||||
order, err := toGlobalOrder(unfilledOrder)
|
||||
assert.NoError(err)
|
||||
assert.Equal(expOrder, order)
|
||||
})
|
||||
|
||||
t.Run("succeeds with limit sell", func(t *testing.T) {
|
||||
newUnfilledOrder := unfilledOrder
|
||||
newUnfilledOrder.Side = v2.SideTypeSell
|
||||
|
||||
newExpOrder := *expOrder
|
||||
newExpOrder.Side = types.SideTypeSell
|
||||
|
||||
order, err := toGlobalOrder(newUnfilledOrder)
|
||||
assert.NoError(err)
|
||||
assert.Equal(&newExpOrder, order)
|
||||
})
|
||||
|
||||
t.Run("succeeds with market sell", func(t *testing.T) {
|
||||
newUnfilledOrder := unfilledOrder
|
||||
newUnfilledOrder.Side = v2.SideTypeSell
|
||||
newUnfilledOrder.OrderType = v2.OrderTypeMarket
|
||||
|
||||
newExpOrder := *expOrder
|
||||
newExpOrder.Side = types.SideTypeSell
|
||||
newExpOrder.Type = types.OrderTypeMarket
|
||||
newExpOrder.Price = newUnfilledOrder.PriceAvg
|
||||
|
||||
order, err := toGlobalOrder(newUnfilledOrder)
|
||||
assert.NoError(err)
|
||||
assert.Equal(&newExpOrder, order)
|
||||
})
|
||||
|
||||
t.Run("succeeds with market buy", func(t *testing.T) {
|
||||
newUnfilledOrder := unfilledOrder
|
||||
newUnfilledOrder.Side = v2.SideTypeBuy
|
||||
newUnfilledOrder.OrderType = v2.OrderTypeMarket
|
||||
|
||||
newExpOrder := *expOrder
|
||||
newExpOrder.Side = types.SideTypeBuy
|
||||
newExpOrder.Type = types.OrderTypeMarket
|
||||
newExpOrder.Price = newUnfilledOrder.PriceAvg
|
||||
newExpOrder.Quantity = newUnfilledOrder.BaseVolume
|
||||
|
||||
order, err := toGlobalOrder(newUnfilledOrder)
|
||||
assert.NoError(err)
|
||||
assert.Equal(&newExpOrder, order)
|
||||
})
|
||||
|
||||
t.Run("succeeds with limit buy", func(t *testing.T) {
|
||||
order, err := toGlobalOrder(unfilledOrder)
|
||||
assert.NoError(err)
|
||||
assert.Equal(&types.Order{
|
||||
SubmitOrder: types.SubmitOrder{
|
||||
ClientOrderID: "74b86af3-6098-479c-acac-bfb074c067f3",
|
||||
Symbol: "BTCUSDT",
|
||||
Side: types.SideTypeBuy,
|
||||
Type: types.OrderTypeLimit,
|
||||
Quantity: fixedpoint.NewFromFloat(5),
|
||||
Price: fixedpoint.NewFromFloat(1.2),
|
||||
TimeInForce: types.TimeInForceGTC,
|
||||
},
|
||||
Exchange: types.ExchangeBitget,
|
||||
OrderID: uint64(orderId),
|
||||
UUID: strconv.FormatInt(int64(orderId), 10),
|
||||
Status: types.OrderStatusFilled,
|
||||
ExecutedQuantity: fixedpoint.NewFromFloat(5),
|
||||
IsWorking: false,
|
||||
CreationTime: types.Time(types.NewMillisecondTimestampFromInt(1660704288118).Time()),
|
||||
UpdateTime: types.Time(types.NewMillisecondTimestampFromInt(1660704288118).Time()),
|
||||
}, order)
|
||||
})
|
||||
|
||||
t.Run("failed to convert side", func(t *testing.T) {
|
||||
newOrder := unfilledOrder
|
||||
newOrder.Side = "xxx"
|
||||
|
||||
_, err := toGlobalOrder(newOrder)
|
||||
assert.ErrorContains(err, "xxx")
|
||||
})
|
||||
|
||||
t.Run("failed to convert oder type", func(t *testing.T) {
|
||||
newOrder := unfilledOrder
|
||||
newOrder.OrderType = "xxx"
|
||||
|
||||
_, err := toGlobalOrder(newOrder)
|
||||
assert.ErrorContains(err, "xxx")
|
||||
})
|
||||
|
||||
t.Run("failed to convert oder status", func(t *testing.T) {
|
||||
newOrder := unfilledOrder
|
||||
newOrder.Status = "xxx"
|
||||
|
||||
_, err := toGlobalOrder(newOrder)
|
||||
assert.ErrorContains(err, "xxx")
|
||||
})
|
||||
}
|
||||
|
||||
func Test_processMarketBuyQuantity(t *testing.T) {
|
||||
var (
|
||||
assert = assert.New(t)
|
||||
filledBaseCoinQty = fixedpoint.NewFromFloat(3.5648)
|
||||
filledPrice = fixedpoint.NewFromFloat(4.99998848)
|
||||
priceAvg = fixedpoint.NewFromFloat(1.4026)
|
||||
buyQty = fixedpoint.NewFromFloat(5)
|
||||
)
|
||||
|
||||
t.Run("zero quantity on Init/New/Live/Cancelled", func(t *testing.T) {
|
||||
qty, err := processMarketBuyQuantity(filledBaseCoinQty, filledPrice, priceAvg, buyQty, v2.OrderStatusInit)
|
||||
assert.NoError(err)
|
||||
assert.Equal(fixedpoint.Zero, qty)
|
||||
|
||||
qty, err = processMarketBuyQuantity(filledBaseCoinQty, filledPrice, priceAvg, buyQty, v2.OrderStatusNew)
|
||||
assert.NoError(err)
|
||||
assert.Equal(fixedpoint.Zero, qty)
|
||||
|
||||
qty, err = processMarketBuyQuantity(filledBaseCoinQty, filledPrice, priceAvg, buyQty, v2.OrderStatusLive)
|
||||
assert.NoError(err)
|
||||
assert.Equal(fixedpoint.Zero, qty)
|
||||
|
||||
qty, err = processMarketBuyQuantity(filledBaseCoinQty, filledPrice, priceAvg, buyQty, v2.OrderStatusCancelled)
|
||||
assert.NoError(err)
|
||||
assert.Equal(fixedpoint.Zero, qty)
|
||||
})
|
||||
|
||||
t.Run("5 on PartialFilled", func(t *testing.T) {
|
||||
priceAvg := fixedpoint.NewFromFloat(2)
|
||||
buyQty := fixedpoint.NewFromFloat(10)
|
||||
filledPrice := fixedpoint.NewFromFloat(4)
|
||||
filledBaseCoinQty := fixedpoint.NewFromFloat(2)
|
||||
|
||||
qty, err := processMarketBuyQuantity(filledBaseCoinQty, filledPrice, priceAvg, buyQty, v2.OrderStatusPartialFilled)
|
||||
assert.NoError(err)
|
||||
assert.Equal(fixedpoint.NewFromFloat(5), qty)
|
||||
})
|
||||
|
||||
t.Run("3.5648 on Filled", func(t *testing.T) {
|
||||
qty, err := processMarketBuyQuantity(filledBaseCoinQty, filledPrice, priceAvg, buyQty, v2.OrderStatusFilled)
|
||||
assert.NoError(err)
|
||||
assert.Equal(fixedpoint.NewFromFloat(3.5648), qty)
|
||||
})
|
||||
|
||||
t.Run("unexpected order status", func(t *testing.T) {
|
||||
_, err := processMarketBuyQuantity(filledBaseCoinQty, filledPrice, priceAvg, buyQty, "xxx")
|
||||
assert.ErrorContains(err, "xxx")
|
||||
})
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"go.uber.org/multierr"
|
||||
"golang.org/x/time/rate"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/exchange/bitget/bitgetapi"
|
||||
|
@ -19,7 +20,9 @@ const (
|
|||
|
||||
PlatformToken = "BGB"
|
||||
|
||||
queryOpenOrdersLimit = 100
|
||||
queryLimit = 100
|
||||
maxOrderIdLen = 36
|
||||
queryMaxDuration = 90 * 24 * time.Hour
|
||||
)
|
||||
|
||||
var log = logrus.WithFields(logrus.Fields{
|
||||
|
@ -37,6 +40,8 @@ var (
|
|||
queryTickersRateLimiter = rate.NewLimiter(rate.Every(time.Second/10), 5)
|
||||
// queryOpenOrdersRateLimiter has its own rate limit. https://www.bitget.com/zh-CN/api-doc/spot/trade/Get-Unfilled-Orders
|
||||
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)
|
||||
)
|
||||
|
||||
type Exchange struct {
|
||||
|
@ -192,7 +197,7 @@ func (e *Exchange) QueryOpenOrders(ctx context.Context, symbol string) (orders [
|
|||
|
||||
req := e.v2Client.NewGetUnfilledOrdersRequest().
|
||||
Symbol(symbol).
|
||||
Limit(strconv.FormatInt(queryOpenOrdersLimit, 10))
|
||||
Limit(strconv.FormatInt(queryLimit, 10))
|
||||
if nextCursor != 0 {
|
||||
req.IdLessThan(strconv.FormatInt(int64(nextCursor), 10))
|
||||
}
|
||||
|
@ -213,11 +218,11 @@ func (e *Exchange) QueryOpenOrders(ctx context.Context, symbol string) (orders [
|
|||
|
||||
orderLen := len(openOrders)
|
||||
// a defensive programming to ensure the length of order response is expected.
|
||||
if orderLen > queryOpenOrdersLimit {
|
||||
if orderLen > queryLimit {
|
||||
return nil, fmt.Errorf("unexpected open orders length %d", orderLen)
|
||||
}
|
||||
|
||||
if orderLen < queryOpenOrdersLimit {
|
||||
if orderLen < queryLimit {
|
||||
break
|
||||
}
|
||||
nextCursor = openOrders[orderLen-1].OrderId
|
||||
|
@ -226,6 +231,57 @@ func (e *Exchange) QueryOpenOrders(ctx context.Context, symbol string) (orders [
|
|||
return orders, nil
|
||||
}
|
||||
|
||||
// QueryClosedOrders queries closed order by time range(`CTime`) and id. The order of the response is in descending order.
|
||||
// If you need to retrieve all data, please utilize the function pkg/exchange/batch.ClosedOrderBatchQuery.
|
||||
//
|
||||
// ** Since is inclusive, Until is exclusive. If you use a time range to query, you must provide both a start time and an end time. **
|
||||
// ** 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 {
|
||||
return nil, fmt.Errorf("start time from the last 90 days can be queried, got: %s", since)
|
||||
}
|
||||
if until.Before(since) {
|
||||
return nil, fmt.Errorf("end time %s before start %s", until, since)
|
||||
}
|
||||
if until.Sub(since) > queryMaxDuration {
|
||||
return nil, fmt.Errorf("the start time %s and end time %s cannot exceed 90 days", since, until)
|
||||
}
|
||||
if lastOrderID != 0 {
|
||||
log.Warn("!!!BITGET EXCHANGE API NOTICE!!! The order of response is in descending order, so the last order id not supported.")
|
||||
}
|
||||
|
||||
if err := closedQueryOrdersRateLimiter.Wait(ctx); err != nil {
|
||||
return nil, fmt.Errorf("query closed order rate limiter wait error: %w", err)
|
||||
}
|
||||
res, err := e.v2Client.NewGetHistoryOrdersRequest().
|
||||
Symbol(symbol).
|
||||
Limit(strconv.Itoa(queryLimit)).
|
||||
StartTime(since.UnixMilli()).
|
||||
EndTime(until.UnixMilli()).
|
||||
Do(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to call get order histories error: %w", err)
|
||||
}
|
||||
|
||||
for _, order := range res {
|
||||
o, err2 := toGlobalOrder(order)
|
||||
if err2 != nil {
|
||||
err = multierr.Append(err, err2)
|
||||
continue
|
||||
}
|
||||
|
||||
if o.Status.Closed() {
|
||||
orders = append(orders, *o)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return types.SortOrdersAscending(orders), nil
|
||||
}
|
||||
|
||||
func (e *Exchange) CancelOrders(ctx context.Context, orders ...types.Order) error {
|
||||
// TODO implement me
|
||||
panic("implement me")
|
||||
|
|
Loading…
Reference in New Issue
Block a user