diff --git a/pkg/cmd/orders.go b/pkg/cmd/orders.go index 0d8719828..6a27e65a4 100644 --- a/pkg/cmd/orders.go +++ b/pkg/cmd/orders.go @@ -4,15 +4,18 @@ import ( "context" "fmt" + "github.com/google/uuid" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" + "github.com/c9s/bbgo/pkg/exchange/ftx" "github.com/c9s/bbgo/pkg/types" + "github.com/c9s/bbgo/pkg/util" ) -// go run ./cmd/bbgo orders [open|closed] --session=ftx --symbol=BTC/USDT -var ordersCmd = &cobra.Command{ - Use: "orders [status]", +// go run ./cmd/bbgo listorders [open|closed] --session=ftx --symbol=BTC/USDT +var listOrdersCmd = &cobra.Command{ + Use: "listorders [status]", Args: cobra.OnlyValidArgs, // default is open which means we query open orders if you haven't provided args. ValidArgs: []string{"", "open", "closed"}, @@ -48,18 +51,84 @@ var ordersCmd = &cobra.Command{ return err } case "closed": + panic("not implemented") default: return fmt.Errorf("invalid status %s", status) } - log.Infof("%s orders: %+v", status, os) + + for _, o := range os { + log.Infof("%s orders: %+v", status, o) + } return nil }, } -func init() { - ordersCmd.Flags().String("session", "", "the exchange session name for sync") - ordersCmd.Flags().String("symbol", "", "the trading pair, like btcusdt") +// go run ./cmd/bbgo placeorder --session=ftx --symbol=BTC/USDT --side=buy --price= --quantity= +var placeOrderCmd = &cobra.Command{ + Use: "placeorder", + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + session, err := cmd.Flags().GetString("session") + if err != nil { + return fmt.Errorf("can't get session from flags: %w", err) + } + ex, err := newExchange(session) + if err != nil { + return err + } + symbol, err := cmd.Flags().GetString("symbol") + if err != nil { + return fmt.Errorf("can't get the symbol from flags: %w", err) + } + if symbol == "" { + return fmt.Errorf("symbol is not found") + } - RootCmd.AddCommand(ordersCmd) + side, err := cmd.Flags().GetString("side") + if err != nil { + return fmt.Errorf("can't get side: %w", err) + } + price, err := cmd.Flags().GetString("price") + if err != nil { + return fmt.Errorf("can't get price: %w", err) + } + quantity, err := cmd.Flags().GetString("quantity") + if err != nil { + return fmt.Errorf("can't get quantity: %w", err) + } + + so := types.SubmitOrder{ + ClientOrderID: uuid.New().String(), + Symbol: symbol, + Side: types.SideType(ftx.TrimUpperString(side)), + Type: types.OrderTypeLimit, + Quantity: util.MustParseFloat(quantity), + Price: util.MustParseFloat(price), + Market: types.Market{Symbol: symbol}, + TimeInForce: "GTC", + } + co, err := ex.SubmitOrders(ctx, so) + if err != nil { + return err + } + + log.Infof("submitted order: %+v\ncreated order: %+v", so, co[0]) + return nil + }, +} + +func init() { + listOrdersCmd.Flags().String("session", "", "the exchange session name for sync") + listOrdersCmd.Flags().String("symbol", "", "the trading pair, like btcusdt") + + placeOrderCmd.Flags().String("session", "", "the exchange session name for sync") + placeOrderCmd.Flags().String("symbol", "", "the trading pair, like btcusdt") + placeOrderCmd.Flags().String("side", "", "the trading side: buy or sell") + placeOrderCmd.Flags().String("price", "", "the trading price") + placeOrderCmd.Flags().String("quantity", "", "the trading quantity") + + RootCmd.AddCommand(listOrdersCmd) + RootCmd.AddCommand(placeOrderCmd) } diff --git a/pkg/exchange/ftx/convert.go b/pkg/exchange/ftx/convert.go index c72dd274a..f833f2652 100644 --- a/pkg/exchange/ftx/convert.go +++ b/pkg/exchange/ftx/convert.go @@ -21,10 +21,14 @@ func TrimUpperString(original string) string { return strings.ToUpper(strings.TrimSpace(original)) } +func TrimLowerString(original string) string { + return strings.ToLower(strings.TrimSpace(original)) +} + var errUnsupportedOrderStatus = fmt.Errorf("unsupported order status") -func toGlobalOrderFromOpenOrder(r order) (types.Order, error) { - // In exchange/max, it only parses these fields. +func toGlobalOrder(r order) (types.Order, error) { + // In exchange/max/convert.go, it only parses these fields. o := types.Order{ SubmitOrder: types.SubmitOrder{ ClientOrderID: r.ClientId, diff --git a/pkg/exchange/ftx/convert_test.go b/pkg/exchange/ftx/convert_test.go index 6c480eb68..a805d9f2d 100644 --- a/pkg/exchange/ftx/convert_test.go +++ b/pkg/exchange/ftx/convert_test.go @@ -34,7 +34,7 @@ func Test_toGlobalOrderFromOpenOrder(t *testing.T) { var r order assert.NoError(t, json.Unmarshal([]byte(input), &r)) - o, err := toGlobalOrderFromOpenOrder(r) + o, err := toGlobalOrder(r) assert.NoError(t, err) assert.Equal(t, "client-id-123", o.ClientOrderID) assert.Equal(t, "XRP-PERP", o.Symbol) @@ -49,3 +49,50 @@ func Test_toGlobalOrderFromOpenOrder(t *testing.T) { assert.Equal(t, types.OrderStatusPartiallyFilled, o.Status) assert.Equal(t, float64(10), o.ExecutedQuantity) } + +func TestTrimLowerString(t *testing.T) { + type args struct { + original string + } + tests := []struct { + name string + args args + want string + }{ + { + name: "spaces", + args: args{ + original: " ", + }, + want: "", + }, + { + name: "uppercase", + args: args{ + original: " HELLO ", + }, + want: "hello", + }, + { + name: "lowercase", + args: args{ + original: " hello", + }, + want: "hello", + }, + { + name: "upper/lower cases", + args: args{ + original: " heLLo ", + }, + want: "hello", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := TrimLowerString(tt.args.original); got != tt.want { + t.Errorf("TrimLowerString() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/exchange/ftx/exchange.go b/pkg/exchange/ftx/exchange.go index 45078c3f9..2097bfb68 100644 --- a/pkg/exchange/ftx/exchange.go +++ b/pkg/exchange/ftx/exchange.go @@ -97,8 +97,38 @@ func (e *Exchange) QueryWithdrawHistory(ctx context.Context, asset string, since panic("implement me") } -func (e *Exchange) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder) (createdOrders types.OrderSlice, err error) { - panic("implement me") +func (e *Exchange) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder) (types.OrderSlice, error) { + var createdOrders types.OrderSlice + // TODO: currently only support limit and market order + // TODO: support time in force + for _, so := range orders { + if so.TimeInForce != "GTC" { + return createdOrders, fmt.Errorf("unsupported TimeInForce %s. only support GTC", so.TimeInForce) + } + or, err := e.rest.PlaceOrder(ctx, PlaceOrderPayload{ + Market: TrimUpperString(so.Symbol), + Side: TrimLowerString(string(so.Side)), + Price: so.Price, + Type: TrimLowerString(string(so.Type)), + Size: so.Quantity, + ReduceOnly: false, + IOC: false, + PostOnly: false, + ClientID: so.ClientOrderID, + }) + if err != nil { + return createdOrders, fmt.Errorf("failed to place order %+v: %w", so, err) + } + if !or.Success { + return createdOrders, fmt.Errorf("ftx returns placing order failure") + } + globalOrder, err := toGlobalOrder(or.Result) + if err != nil { + return createdOrders, fmt.Errorf("failed to convert response to global order") + } + createdOrders = append(createdOrders, globalOrder) + } + return createdOrders, nil } func (e *Exchange) QueryOpenOrders(ctx context.Context, symbol string) (orders []types.Order, err error) { @@ -111,7 +141,7 @@ func (e *Exchange) QueryOpenOrders(ctx context.Context, symbol string) (orders [ return nil, fmt.Errorf("ftx returns querying open orders failure") } for _, r := range resp.Result { - o, err := toGlobalOrderFromOpenOrder(r) + o, err := toGlobalOrder(r) if err != nil { return nil, err } diff --git a/pkg/exchange/ftx/rest_order_request.go b/pkg/exchange/ftx/rest_order_request.go index ba0c25f8c..fc3467e38 100644 --- a/pkg/exchange/ftx/rest_order_request.go +++ b/pkg/exchange/ftx/rest_order_request.go @@ -10,7 +10,60 @@ type orderRequest struct { *restRequest } -func (r *orderRequest) OpenOrders(ctx context.Context, market string) (orders, error) { +/* +{ + "market": "XRP-PERP", + "side": "sell", + "price": 0.306525, + "type": "limit", + "size": 31431.0, + "reduceOnly": false, + "ioc": false, + "postOnly": false, + "clientId": null +} +*/ +type PlaceOrderPayload struct { + Market string + Side string + Price float64 + Type string + Size float64 + ReduceOnly bool + IOC bool + PostOnly bool + ClientID string +} + +func (r *orderRequest) PlaceOrder(ctx context.Context, p PlaceOrderPayload) (orderResponse, error) { + resp, err := r. + Method("POST"). + ReferenceURL("api/orders"). + Payloads(map[string]interface{}{ + "market": p.Market, + "side": p.Side, + "price": p.Price, + "type": p.Type, + "size": p.Size, + "reduceOnly": p.ReduceOnly, + "ioc": p.IOC, + "postOnly": p.PostOnly, + "clientId": p.ClientID, + }). + DoAuthenticatedRequest(ctx) + + if err != nil { + return orderResponse{}, err + } + var o orderResponse + if err := json.Unmarshal(resp.Body, &o); err != nil { + return orderResponse{}, fmt.Errorf("failed to unmarshal order response body to json: %w", err) + } + + return o, nil +} + +func (r *orderRequest) OpenOrders(ctx context.Context, market string) (ordersResponse, error) { resp, err := r. Method("GET"). ReferenceURL("api/orders"). @@ -18,12 +71,12 @@ func (r *orderRequest) OpenOrders(ctx context.Context, market string) (orders, e DoAuthenticatedRequest(ctx) if err != nil { - return orders{}, err + return ordersResponse{}, err } - var o orders + var o ordersResponse if err := json.Unmarshal(resp.Body, &o); err != nil { - return orders{}, fmt.Errorf("failed to unmarshal open orders response body to json: %w", err) + return ordersResponse{}, fmt.Errorf("failed to unmarshal open orders response body to json: %w", err) } return o, nil diff --git a/pkg/exchange/ftx/rest_responses.go b/pkg/exchange/ftx/rest_responses.go index e0bed2691..a030161bf 100644 --- a/pkg/exchange/ftx/rest_responses.go +++ b/pkg/exchange/ftx/rest_responses.go @@ -12,7 +12,7 @@ type balances struct { } `json:"result"` } -type orders struct { +type ordersResponse struct { Success bool `json:"Success"` Result []order `json:"result"` @@ -37,3 +37,9 @@ type order struct { PostOnly bool `json:"postOnly"` ClientId string `json:"clientId"` } + +type orderResponse struct { + Success bool `json:"Success"` + + Result order `json:"result"` +}