all: improve cancel command and add uuid field to order struct

This commit is contained in:
c9s 2021-12-26 01:27:22 +08:00
parent 471d86c801
commit 0cef2c52ef
7 changed files with 185 additions and 61 deletions

View File

@ -107,4 +107,17 @@ godotenv -f .env.local -- go run ./cmd/bbgo --config config/bbgo.yaml userdatast
```shell
godotenv -f .env.local -- go run ./cmd/bbgo submit-order --session=kucoin --symbol=BTCUSDT --side=buy --price=18000 --quantity=0.001
```
```
### Testing open orders query
```shell
godotenv -f .env.local -- go run ./cmd/bbgo list-orders --session kucoin --symbol=BTCUSDT open
godotenv -f .env.local -- go run ./cmd/bbgo list-orders --session kucoin --symbol=BTCUSDT closed
```
### Testing order cancel
```shell
godotenv -f .env.local -- go run ./cmd/bbgo cancel-order --session kucoin --order-uuid 61c745c44592c200014abdcf
```

View File

@ -23,6 +23,7 @@ func init() {
cancelOrderCmd.Flags().String("symbol", "", "symbol to cancel orders")
cancelOrderCmd.Flags().Int64("group-id", 0, "group ID to cancel orders")
cancelOrderCmd.Flags().Uint64("order-id", 0, "order ID to cancel orders")
cancelOrderCmd.Flags().String("order-uuid", "", "order UUID to cancel orders")
cancelOrderCmd.Flags().Bool("all", false, "cancel all orders")
RootCmd.AddCommand(cancelOrderCmd)
}
@ -54,60 +55,56 @@ var cancelOrderCmd = &cobra.Command{
return err
}
orderUUID, err := cmd.Flags().GetString("order-uuid")
if err != nil {
return err
}
all, err := cmd.Flags().GetBool("all")
if err != nil {
return err
}
configFile, err := cmd.Flags().GetString("config")
if err != nil {
return err
}
if len(configFile) == 0 {
return errors.New("--config option is required")
}
userConfig, err := bbgo.Load(configFile, false)
if err != nil {
return err
}
environ := bbgo.NewEnvironment()
if err := environ.ConfigureDatabase(ctx); err != nil {
return err
}
if err := environ.ConfigureExchangeSessions(userConfig); err != nil {
return err
}
if userConfig.Persistence != nil {
if err := environ.ConfigurePersistence(userConfig.Persistence); err != nil {
return err
}
}
var sessions = environ.Sessions()
sessionName, err := cmd.Flags().GetString("session")
if err != nil {
return err
}
if userConfig == nil {
return errors.New("config file is required")
}
environ := bbgo.NewEnvironment()
if err := environ.ConfigureExchangeSessions(userConfig); err != nil {
return err
}
if err := environ.Init(ctx); err != nil {
return err
}
var sessions = environ.Sessions()
if len(sessionName) > 0 {
ses, ok := sessions[sessionName]
ses, ok := environ.Session(sessionName)
if !ok {
return fmt.Errorf("session %s not found", sessionName)
}
if orderID > 0 {
logrus.Infof("canceling order by the given order id %d", orderID)
if orderID > 0 || orderUUID != "" {
if orderID > 0 {
logrus.Infof("canceling order by the given order id %d", orderID)
} else if orderUUID != "" {
logrus.Infof("canceling order by the given order uuid %s", orderUUID)
}
err := ses.Exchange.CancelOrders(ctx, types.Order{
SubmitOrder: types.SubmitOrder{
Symbol: symbol,
},
OrderID: orderID,
UUID: orderUUID,
})
if err != nil {
return err

View File

@ -5,15 +5,17 @@ import (
"fmt"
"os"
"os/signal"
"strings"
"syscall"
"time"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/google/uuid"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/exchange/ftx"
"github.com/c9s/bbgo/pkg/types"
@ -106,8 +108,9 @@ var listOrdersCmd = &cobra.Command{
return fmt.Errorf("invalid status %s", status)
}
log.Infof("%s ORDERS FROM %s SESSION", strings.ToUpper(status), session.Name)
for _, o := range os {
log.Infof("%s orders: %+v", status, o)
log.Infof("%+v", o)
}
return nil

View File

@ -130,6 +130,21 @@ func hashStringID(s string) uint64 {
return h.Sum64()
}
func toGlobalOrderStatus(o kucoinapi.Order) types.OrderStatus {
var status types.OrderStatus
if o.IsActive {
status = types.OrderStatusNew
} else if o.DealSize > 0 {
status = types.OrderStatusPartiallyFilled
} else if o.CancelExist {
status = types.OrderStatusCanceled
} else {
status = types.OrderStatusFilled
}
return status
}
func toGlobalSide(s string) types.SideType {
switch s {
case "buy":
@ -173,3 +188,27 @@ func toLocalSide(side types.SideType) kucoinapi.SideType {
return ""
}
func toGlobalOrder(o kucoinapi.Order) types.Order {
var status = toGlobalOrderStatus(o)
var order = types.Order{
SubmitOrder: types.SubmitOrder{
ClientOrderID: o.ClientOrderID,
Symbol: toGlobalSymbol(o.Symbol),
Side: toGlobalSide(o.Side),
Type: toGlobalOrderType(o.Type),
Quantity: o.Size.Float64(),
Price: o.Price.Float64(),
StopPrice: o.StopPrice.Float64(),
TimeInForce: string(o.TimeInForce),
},
Exchange: types.ExchangeKucoin,
OrderID: hashStringID(o.ID),
UUID: o.ID,
Status: status,
ExecutedQuantity: o.DealSize.Float64(),
IsWorking: o.IsActive,
CreationTime: types.Time(o.CreatedAt.Time()),
UpdateTime: types.Time(o.CreatedAt.Time()), // kucoin does not response updated time
}
return order
}

View File

@ -5,10 +5,12 @@ import (
"strconv"
"time"
"github.com/c9s/bbgo/pkg/exchange/kucoin/kucoinapi"
"github.com/c9s/bbgo/pkg/types"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"go.uber.org/multierr"
"github.com/c9s/bbgo/pkg/exchange/kucoin/kucoinapi"
"github.com/c9s/bbgo/pkg/types"
)
var ErrMissingSequence = errors.New("sequence is missing")
@ -185,16 +187,67 @@ func (e *Exchange) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder
return createdOrders, err
}
// QueryOpenOrders
/*
Documentation from the Kucoin API page
Any order on the exchange order book is in active status.
Orders removed from the order book will be marked with done status.
After an order becomes done, there may be a few milliseconds latency before its fully settled.
You can check the orders in any status.
If the status parameter is not specified, orders of done status will be returned by default.
When you query orders in active status, there is no time limit.
However, when you query orders in done status, the start and end time range cannot exceed 7* 24 hours.
An error will occur if the specified time window exceeds the range.
If you specify the end time only, the system will automatically calculate the start time as end time minus 7*24 hours, and vice versa.
The history for cancelled orders is only kept for one month.
You will not be able to query for cancelled orders that have happened more than a month ago.
*/
func (e *Exchange) QueryOpenOrders(ctx context.Context, symbol string) (orders []types.Order, err error) {
req := e.client.TradeService.NewListOrdersRequest()
req.Symbol(toLocalSymbol(symbol))
req.Status("active")
orderList, err := req.Do(ctx)
if err != nil {
return nil, err
}
return nil, nil
// TODO: support pagination (right now we can only get 50 items from the first page)
for _, o := range orderList.Items {
order := toGlobalOrder(o)
orders = append(orders, order)
}
return orders, err
}
func (e *Exchange) CancelOrders(ctx context.Context, orders ...types.Order) error {
panic("implement me")
return nil
func (e *Exchange) CancelOrders(ctx context.Context, orders ...types.Order) (errs error) {
for _, o := range orders {
req := e.client.TradeService.NewCancelOrderRequest()
if o.UUID != "" {
req.OrderID(o.UUID)
} else if o.ClientOrderID != "" {
req.ClientOrderID(o.ClientOrderID)
} else {
errs = multierr.Append(errs, errors.New("can not cancel order, either order uuid nor client order id is empty"))
continue
}
response, err := req.Do(ctx)
if err != nil {
errs = multierr.Append(errs, err)
continue
}
log.Infof("cancelled orders: %v", response.CancelledOrderIDs)
}
return errs
}
func (e *Exchange) NewStream() types.Stream {

View File

@ -6,12 +6,13 @@ import (
"sync"
"time"
"github.com/gorilla/websocket"
"github.com/pkg/errors"
"github.com/c9s/bbgo/pkg/depth"
"github.com/c9s/bbgo/pkg/exchange/kucoin/kucoinapi"
"github.com/c9s/bbgo/pkg/types"
"github.com/c9s/bbgo/pkg/util"
"github.com/gorilla/websocket"
"github.com/pkg/errors"
)
const readTimeout = 30 * time.Second
@ -27,7 +28,7 @@ type Stream struct {
connCtx context.Context
connCancel context.CancelFunc
bullet *kucoinapi.Bullet
bullet *kucoinapi.Bullet
candleEventCallbacks []func(candle *WebSocketCandleEvent, e *WebSocketEvent)
orderBookL2EventCallbacks []func(e *WebSocketOrderBookL2Event)
tickerEventCallbacks []func(e *WebSocketTickerEvent)
@ -43,8 +44,8 @@ func NewStream(client *kucoinapi.RestClient, ex *Exchange) *Stream {
StandardStream: types.StandardStream{
ReconnectC: make(chan struct{}, 1),
},
client: client,
exchange: ex,
client: client,
exchange: ex,
lastCandle: make(map[string]types.KLine),
depthBuffers: make(map[string]*depth.Buffer),
}
@ -136,9 +137,15 @@ func (s *Stream) handlePrivateOrderEvent(e *WebSocketPrivateOrderEvent) {
case "open", "match", "filled":
status := types.OrderStatusNew
if e.Status == "done" {
status = types.OrderStatusFilled
} else if e.FilledSize > 0 {
status = types.OrderStatusPartiallyFilled
if e.FilledSize == e.Size {
status = types.OrderStatusFilled
} else {
status = types.OrderStatusCanceled
}
} else if e.Status == "open" {
if e.FilledSize > 0 {
status = types.OrderStatusPartiallyFilled
}
}
s.StandardStream.EmitOrderUpdate(types.Order{
@ -151,9 +158,10 @@ func (s *Stream) handlePrivateOrderEvent(e *WebSocketPrivateOrderEvent) {
},
Exchange: types.ExchangeKucoin,
OrderID: hashStringID(e.OrderId),
UUID: e.OrderId,
Status: status,
ExecutedQuantity: e.FilledSize.Float64(),
IsWorking: true,
IsWorking: e.Status == "open",
CreationTime: types.Time(e.OrderTime.Time()),
UpdateTime: types.Time(e.Ts.Time()),
})

View File

@ -175,14 +175,18 @@ func (o *SubmitOrder) SlackAttachment() slack.Attachment {
type Order struct {
SubmitOrder
Exchange ExchangeName `json:"exchange" db:"exchange"`
GID uint64 `json:"gid" db:"gid"`
OrderID uint64 `json:"orderID" db:"order_id"` // order id
Status OrderStatus `json:"status" db:"status"`
ExecutedQuantity float64 `json:"executedQuantity" db:"executed_quantity"`
IsWorking bool `json:"isWorking" db:"is_working"`
CreationTime Time `json:"creationTime" db:"created_at"`
UpdateTime Time `json:"updateTime" db:"updated_at"`
Exchange ExchangeName `json:"exchange" db:"exchange"`
// GID is used for relational database storage, it's an incremental ID
GID uint64 `json:"gid" db:"gid"`
OrderID uint64 `json:"orderID" db:"order_id"` // order id
UUID string `json:"uuid,omitempty"`
Status OrderStatus `json:"status" db:"status"`
ExecutedQuantity float64 `json:"executedQuantity" db:"executed_quantity"`
IsWorking bool `json:"isWorking" db:"is_working"`
CreationTime Time `json:"creationTime" db:"created_at"`
UpdateTime Time `json:"updateTime" db:"updated_at"`
IsMargin bool `json:"isMargin" db:"is_margin"`
IsIsolated bool `json:"isIsolated" db:"is_isolated"`
@ -200,7 +204,14 @@ func (o Order) Backup() SubmitOrder {
}
func (o Order) String() string {
return fmt.Sprintf("ORDER %s %d %s %s %f/%f @ %f -> %s", o.Exchange.String(), o.OrderID, o.Symbol, o.Side, o.ExecutedQuantity, o.Quantity, o.Price, o.Status)
var orderID string
if o.UUID != "" {
orderID = o.UUID
} else {
orderID = strconv.FormatUint(o.OrderID, 10)
}
return fmt.Sprintf("ORDER %s %s %s %s %f/%f @ %f -> %s", o.Exchange.String(), orderID, o.Symbol, o.Side, o.ExecutedQuantity, o.Quantity, o.Price, o.Status)
}
// PlainText is used for telegram-styled messages