Merge pull request #1394 from c9s/edwin/bitget/ClosedOpenOrders

FEATURE: [bitget] support query closed orders
This commit is contained in:
bailantaotao 2023-11-09 09:45:37 +08:00 committed by GitHub
commit 66caf78556
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 792 additions and 7 deletions

View File

@ -2,12 +2,11 @@ package bitgetapi
import ( import (
"context" "context"
"github.com/stretchr/testify/assert"
"os" "os"
"strconv" "strconv"
"testing" "testing"
"github.com/stretchr/testify/assert"
"github.com/c9s/bbgo/pkg/exchange/bitget/bitgetapi" "github.com/c9s/bbgo/pkg/exchange/bitget/bitgetapi"
"github.com/c9s/bbgo/pkg/testutil" "github.com/c9s/bbgo/pkg/testutil"
) )
@ -25,7 +24,6 @@ func getTestClientOrSkip(t *testing.T) *Client {
client := bitgetapi.NewClient() client := bitgetapi.NewClient()
client.Auth(key, secret, os.Getenv("BITGET_API_PASSPHRASE")) client.Auth(key, secret, os.Getenv("BITGET_API_PASSPHRASE"))
return NewClient(client) return NewClient(client)
} }
@ -39,4 +37,12 @@ func TestClient(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
t.Logf("resp: %+v", resp) 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)
})
} }

View 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, unitcurrency obtained from the transaction.
DeductedByCoupon fixedpoint.Value `json:"c"`
// Amount deducted in BGB (Bitget Coin), unitBGB
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}
}

View File

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

View File

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

View File

@ -1,6 +1,7 @@
package bitget package bitget
import ( import (
"errors"
"fmt" "fmt"
"math" "math"
"strconv" "strconv"
@ -158,3 +159,87 @@ func unfilledOrderToGlobalOrder(order v2.UnfilledOrder) (*types.Order, error) {
UpdateTime: types.Time(order.UTime.Time()), UpdateTime: types.Time(order.UTime.Time()),
}, nil }, 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)
}
}

View File

@ -272,3 +272,196 @@ func Test_unfilledOrderToGlobalOrder(t *testing.T) {
assert.ErrorContains(err, "xxx") 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")
})
}

View File

@ -7,6 +7,7 @@ import (
"time" "time"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"go.uber.org/multierr"
"golang.org/x/time/rate" "golang.org/x/time/rate"
"github.com/c9s/bbgo/pkg/exchange/bitget/bitgetapi" "github.com/c9s/bbgo/pkg/exchange/bitget/bitgetapi"
@ -19,7 +20,9 @@ const (
PlatformToken = "BGB" PlatformToken = "BGB"
queryOpenOrdersLimit = 100 queryLimit = 100
maxOrderIdLen = 36
queryMaxDuration = 90 * 24 * time.Hour
) )
var log = logrus.WithFields(logrus.Fields{ var log = logrus.WithFields(logrus.Fields{
@ -37,6 +40,8 @@ var (
queryTickersRateLimiter = rate.NewLimiter(rate.Every(time.Second/10), 5) 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 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) 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 { type Exchange struct {
@ -192,7 +197,7 @@ func (e *Exchange) QueryOpenOrders(ctx context.Context, symbol string) (orders [
req := e.v2Client.NewGetUnfilledOrdersRequest(). req := e.v2Client.NewGetUnfilledOrdersRequest().
Symbol(symbol). Symbol(symbol).
Limit(strconv.FormatInt(queryOpenOrdersLimit, 10)) Limit(strconv.FormatInt(queryLimit, 10))
if nextCursor != 0 { if nextCursor != 0 {
req.IdLessThan(strconv.FormatInt(int64(nextCursor), 10)) req.IdLessThan(strconv.FormatInt(int64(nextCursor), 10))
} }
@ -213,11 +218,11 @@ func (e *Exchange) QueryOpenOrders(ctx context.Context, symbol string) (orders [
orderLen := len(openOrders) orderLen := len(openOrders)
// a defensive programming to ensure the length of order response is expected. // 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) return nil, fmt.Errorf("unexpected open orders length %d", orderLen)
} }
if orderLen < queryOpenOrdersLimit { if orderLen < queryLimit {
break break
} }
nextCursor = openOrders[orderLen-1].OrderId nextCursor = openOrders[orderLen-1].OrderId
@ -226,6 +231,57 @@ func (e *Exchange) QueryOpenOrders(ctx context.Context, symbol string) (orders [
return orders, nil 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 { func (e *Exchange) CancelOrders(ctx context.Context, orders ...types.Order) error {
// TODO implement me // TODO implement me
panic("implement me") panic("implement me")