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

@ -108,3 +108,16 @@ godotenv -f .env.local -- go run ./cmd/bbgo --config config/bbgo.yaml userdatast
```shell ```shell
godotenv -f .env.local -- go run ./cmd/bbgo submit-order --session=kucoin --symbol=BTCUSDT --side=buy --price=18000 --quantity=0.001 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().String("symbol", "", "symbol to cancel orders")
cancelOrderCmd.Flags().Int64("group-id", 0, "group ID 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().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") cancelOrderCmd.Flags().Bool("all", false, "cancel all orders")
RootCmd.AddCommand(cancelOrderCmd) RootCmd.AddCommand(cancelOrderCmd)
} }
@ -54,60 +55,56 @@ var cancelOrderCmd = &cobra.Command{
return err return err
} }
orderUUID, err := cmd.Flags().GetString("order-uuid")
if err != nil {
return err
}
all, err := cmd.Flags().GetBool("all") all, err := cmd.Flags().GetBool("all")
if err != nil { if err != nil {
return err 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") sessionName, err := cmd.Flags().GetString("session")
if err != nil { if err != nil {
return err 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 { if len(sessionName) > 0 {
ses, ok := sessions[sessionName] ses, ok := environ.Session(sessionName)
if !ok { if !ok {
return fmt.Errorf("session %s not found", sessionName) return fmt.Errorf("session %s not found", sessionName)
} }
if orderID > 0 { if orderID > 0 || orderUUID != "" {
logrus.Infof("canceling order by the given order id %d", orderID) 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{ err := ses.Exchange.CancelOrders(ctx, types.Order{
SubmitOrder: types.SubmitOrder{ SubmitOrder: types.SubmitOrder{
Symbol: symbol, Symbol: symbol,
}, },
OrderID: orderID, OrderID: orderID,
UUID: orderUUID,
}) })
if err != nil { if err != nil {
return err return err

View File

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

View File

@ -130,6 +130,21 @@ func hashStringID(s string) uint64 {
return h.Sum64() 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 { func toGlobalSide(s string) types.SideType {
switch s { switch s {
case "buy": case "buy":
@ -173,3 +188,27 @@ func toLocalSide(side types.SideType) kucoinapi.SideType {
return "" 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" "strconv"
"time" "time"
"github.com/c9s/bbgo/pkg/exchange/kucoin/kucoinapi"
"github.com/c9s/bbgo/pkg/types"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus" "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") var ErrMissingSequence = errors.New("sequence is missing")
@ -185,16 +187,67 @@ func (e *Exchange) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder
return createdOrders, err 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) { func (e *Exchange) QueryOpenOrders(ctx context.Context, symbol string) (orders []types.Order, err error) {
req := e.client.TradeService.NewListOrdersRequest() req := e.client.TradeService.NewListOrdersRequest()
req.Symbol(toLocalSymbol(symbol)) 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 { func (e *Exchange) CancelOrders(ctx context.Context, orders ...types.Order) (errs error) {
panic("implement me") for _, o := range orders {
return nil 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 { func (e *Exchange) NewStream() types.Stream {

View File

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

View File

@ -175,14 +175,18 @@ func (o *SubmitOrder) SlackAttachment() slack.Attachment {
type Order struct { type Order struct {
SubmitOrder SubmitOrder
Exchange ExchangeName `json:"exchange" db:"exchange"` Exchange ExchangeName `json:"exchange" db:"exchange"`
GID uint64 `json:"gid" db:"gid"`
OrderID uint64 `json:"orderID" db:"order_id"` // order id // GID is used for relational database storage, it's an incremental ID
Status OrderStatus `json:"status" db:"status"` GID uint64 `json:"gid" db:"gid"`
ExecutedQuantity float64 `json:"executedQuantity" db:"executed_quantity"` OrderID uint64 `json:"orderID" db:"order_id"` // order id
IsWorking bool `json:"isWorking" db:"is_working"` UUID string `json:"uuid,omitempty"`
CreationTime Time `json:"creationTime" db:"created_at"`
UpdateTime Time `json:"updateTime" db:"updated_at"` 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"` IsMargin bool `json:"isMargin" db:"is_margin"`
IsIsolated bool `json:"isIsolated" db:"is_isolated"` IsIsolated bool `json:"isIsolated" db:"is_isolated"`
@ -200,7 +204,14 @@ func (o Order) Backup() SubmitOrder {
} }
func (o Order) String() string { 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 // PlainText is used for telegram-styled messages