mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-25 16:25:16 +00:00
Merge pull request #149 from c9s/ftx/place-order
This commit is contained in:
commit
7dd67a8725
|
@ -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=<price> --quantity=<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)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user