From 0c854a8a855bdb6af6dc9cb10063e0b4fc1460ad Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 11 Dec 2021 19:40:53 +0800 Subject: [PATCH] kucoin: add place order and list orders command --- examples/kucoin/orders.go | 131 +++++++ pkg/exchange/kucoin/kucoinapi/client.go | 22 +- pkg/exchange/kucoin/kucoinapi/trade.go | 459 +++++++++--------------- 3 files changed, 312 insertions(+), 300 deletions(-) create mode 100644 examples/kucoin/orders.go diff --git a/examples/kucoin/orders.go b/examples/kucoin/orders.go new file mode 100644 index 000000000..7fd6996f6 --- /dev/null +++ b/examples/kucoin/orders.go @@ -0,0 +1,131 @@ +package main + +import ( + "context" + + "github.com/c9s/bbgo/pkg/exchange/kucoin/kucoinapi" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +// go run ./examples/kucoin orders +var ordersCmd = &cobra.Command{ + Use: "orders", + + // SilenceUsage is an option to silence usage when an error occurs. + SilenceUsage: true, + + RunE: func(cmd *cobra.Command, args []string) error { + req := client.TradeService.NewListOrdersRequest() + + symbol, err := cmd.Flags().GetString("symbol") + if err != nil { + return err + } + + if len(symbol) == 0 { + return errors.New("--symbol option is required") + } + + req.Symbol(symbol) + + + status, err := cmd.Flags().GetString("status") + if err != nil { + return err + } + + if len(status) > 0 { + req.Status(status) + } + + page, err := req.Do(context.Background()) + if err != nil { + return err + } + + logrus.Infof("page: %+v", page) + return nil + }, +} + +func init() { + ordersCmd.Flags().String("symbol", "", "symbol, BTC-USDT, LTC-USDT...etc") + ordersCmd.Flags().String("status", "", "status, active or done") + rootCmd.AddCommand(ordersCmd) + + placeOrderCmd.Flags().String("symbol", "", "symbol") + placeOrderCmd.Flags().String("price", "", "price") + placeOrderCmd.Flags().String("size", "", "size") + placeOrderCmd.Flags().String("order-type", string(kucoinapi.OrderTypeLimit), "order type") + placeOrderCmd.Flags().String("side", "", "buy or sell") + ordersCmd.AddCommand(placeOrderCmd) +} + +// usage: +// go run ./examples/kucoin orders place --symbol LTC-USDT --price 50 --size 1 --order-type limit --side buy +var placeOrderCmd = &cobra.Command{ + Use: "place", + + // SilenceUsage is an option to silence usage when an error occurs. + SilenceUsage: true, + + RunE: func(cmd *cobra.Command, args []string) error { + req := client.TradeService.NewPlaceOrderRequest() + + orderType, err := cmd.Flags().GetString("order-type") + if err != nil { + return err + } + + req.OrderType(kucoinapi.OrderType(orderType)) + + + side, err := cmd.Flags().GetString("side") + if err != nil { + return err + } + req.Side(kucoinapi.SideType(side)) + + + symbol, err := cmd.Flags().GetString("symbol") + if err != nil { + return err + } + + if len(symbol) == 0 { + return errors.New("--symbol is required") + } + + req.Symbol(symbol) + + switch kucoinapi.OrderType(orderType) { + case kucoinapi.OrderTypeLimit: + price, err := cmd.Flags().GetString("price") + if err != nil { + return err + } + req.Price(price) + + case kucoinapi.OrderTypeMarket: + + } + + + size, err := cmd.Flags().GetString("size") + if err != nil { + return err + } + req.Size(size) + + response, err := req.Do(context.Background()) + if err != nil { + return err + } + + logrus.Infof("place order response: %+v", response) + return nil + }, +} + diff --git a/pkg/exchange/kucoin/kucoinapi/client.go b/pkg/exchange/kucoin/kucoinapi/client.go index 3446d44f5..e76a7c803 100644 --- a/pkg/exchange/kucoin/kucoinapi/client.go +++ b/pkg/exchange/kucoin/kucoinapi/client.go @@ -17,13 +17,19 @@ import ( "github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/util" "github.com/pkg/errors" - log "github.com/sirupsen/logrus" ) const defaultHTTPTimeout = time.Second * 15 const RestBaseURL = "https://api.kucoin.com/api" const SandboxRestBaseURL = "https://openapi-sandbox.kucoin.com/api" +type TradeType string + +const ( + TradeTypeSpot TradeType = "TRADE" + TradeTypeMargin TradeType = "MARGIN" +) + type SideType string const ( @@ -50,8 +56,8 @@ const ( type OrderType string const ( - OrderTypeMarket OrderType = "market" - OrderTypeLimit OrderType = "limit" + OrderTypeMarket OrderType = "market" + OrderTypeLimit OrderType = "limit" ) type InstrumentType string @@ -78,9 +84,9 @@ type RestClient struct { client *http.Client Key, Secret, Passphrase string - KeyVersion string + KeyVersion string - AccountService *AccountService + AccountService *AccountService MarketDataService *MarketDataService TradeService *TradeService } @@ -92,14 +98,14 @@ func NewClient() *RestClient { } client := &RestClient{ - BaseURL: u, + BaseURL: u, KeyVersion: "2", client: &http.Client{ Timeout: defaultHTTPTimeout, }, } - client.AccountService = &AccountService{ client: client } + client.AccountService = &AccountService{client: client} client.MarketDataService = &MarketDataService{client: client} client.TradeService = &TradeService{client: client} return client @@ -210,8 +216,6 @@ func (c *RestClient) newAuthenticatedRequest(method, refURL string, params url.V req.Header.Add("KC-API-TIMESTAMP", timestamp) req.Header.Add("KC-API-PASSPHRASE", sign(c.Secret, c.Passphrase)) req.Header.Add("KC-API-KEY-VERSION", c.KeyVersion) - - log.Infof("%+v", req.Header) return req, nil } diff --git a/pkg/exchange/kucoin/kucoinapi/trade.go b/pkg/exchange/kucoin/kucoinapi/trade.go index 1f8f06099..1ea93b5c4 100644 --- a/pkg/exchange/kucoin/kucoinapi/trade.go +++ b/pkg/exchange/kucoin/kucoinapi/trade.go @@ -3,10 +3,12 @@ package kucoinapi import ( "context" "net/url" - "strings" + "strconv" + "time" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" + "github.com/google/uuid" "github.com/pkg/errors" ) @@ -42,22 +44,146 @@ func (c *TradeService) NewCancelAllOrderRequest() *CancelAllOrderRequest { } } -func (c *TradeService) NewGetOrderDetailsRequest() *GetOrderDetailsRequest { - return &GetOrderDetailsRequest{ - client: c.client, - } +type ListOrdersRequest struct { + client *RestClient + + status *string + + symbol *string + + side *SideType + + orderType *OrderType + + tradeType *TradeType + + startAt *time.Time + + endAt *time.Time } -func (c *TradeService) NewGetPendingOrderRequest() *GetPendingOrderRequest { - return &GetPendingOrderRequest{ - client: c.client, - } +func (r *ListOrdersRequest) Status(status string) { + r.status = &status } -func (c *TradeService) NewGetTransactionDetailsRequest() *GetTransactionDetailsRequest { - return &GetTransactionDetailsRequest{ - client: c.client, +func (r *ListOrdersRequest) Symbol(symbol string) { + r.symbol = &symbol +} + +func (r *ListOrdersRequest) Side(side SideType) { + r.side = &side +} + +func (r *ListOrdersRequest) OrderType(orderType OrderType) { + r.orderType = &orderType +} + +func (r *ListOrdersRequest) StartAt(startAt time.Time) { + r.startAt = &startAt +} + +func (r *ListOrdersRequest) EndAt(endAt time.Time) { + r.endAt = &endAt +} + +type Order struct { + ID string `json:"id"` + Symbol string `json:"symbol"` + OperationType string `json:"opType"` + Type string `json:"type"` + Side string `json:"side"` + Price fixedpoint.Value `json:"price"` + Size fixedpoint.Value `json:"size"` + Funds fixedpoint.Value `json:"funds"` + DealFunds fixedpoint.Value `json:"dealFunds"` + DealSize fixedpoint.Value `json:"dealSize"` + Fee fixedpoint.Value `json:"fee"` + FeeCurrency string `json:"feeCurrency"` + StopType string `json:"stop"` + StopTriggerred bool `json:"stopTriggered"` + StopPrice fixedpoint.Value `json:"stopPrice"` + TimeInForce TimeInForceType `json:"timeInForce"` + PostOnly bool `json:"postOnly"` + Hidden bool `json:"hidden"` + Iceberg bool `json:"iceberg"` + Channel string `json:"channel"` + ClientOrderID string `json:"clientOid"` + Remark string `json:"remark"` + IsActive bool `json:"isActive"` + CancelExist bool `json:"cancelExist"` + CreatedAt types.MillisecondTimestamp `json:"createdAt"` +} + +type OrderListPage struct { + CurrentPage int `json:"currentPage"` + PageSize int `json:"pageSize"` + TotalNumber int `json:"totalNum"` + TotalPage int `json:"totalPage"` + Items []Order `json:"items"` +} + +func (r *ListOrdersRequest) Do(ctx context.Context) (*OrderListPage, error) { + var params = url.Values{} + + if r.status != nil { + params["status"] = []string{*r.status} } + + if r.symbol != nil { + params["symbol"] = []string{*r.symbol} + } + + if r.side != nil { + params["side"] = []string{string(*r.side)} + } + + if r.orderType != nil { + params["type"] = []string{string(*r.orderType)} + } + + if r.tradeType != nil { + params["tradeType"] = []string{string(*r.tradeType)} + } else { + params["tradeType"] = []string{"TRADE"} + } + + if r.startAt != nil { + params["startAt"] = []string{strconv.FormatInt(r.startAt.UnixNano()/int64(time.Millisecond), 10)} + } + + if r.endAt != nil { + params["endAt"] = []string{strconv.FormatInt(r.endAt.UnixNano()/int64(time.Millisecond), 10)} + } + + req, err := r.client.newAuthenticatedRequest("GET", "/api/v1/orders", params, nil) + if err != nil { + return nil, err + } + + response, err := r.client.sendRequest(req) + if err != nil { + return nil, err + } + + var orderResponse struct { + Code string `json:"code"` + Message string `json:"msg"` + Data *OrderListPage `json:"data"` + } + + if err := response.DecodeJSON(&orderResponse); err != nil { + return nil, err + } + + if orderResponse.Data == nil { + return nil, errors.New("api error: [" + orderResponse.Code + "] " + orderResponse.Message) + } + + return orderResponse.Data, nil +} + +func (c *TradeService) NewListOrdersRequest() *ListOrdersRequest { + return &ListOrdersRequest{client: c.client} } type PlaceOrderRequest struct { @@ -99,8 +225,8 @@ func (r *PlaceOrderRequest) Side(side SideType) *PlaceOrderRequest { return r } -func (r *PlaceOrderRequest) Quantity(quantity string) *PlaceOrderRequest { - r.size = quantity +func (r *PlaceOrderRequest) Size(size string) *PlaceOrderRequest { + r.size = size return r } @@ -119,13 +245,19 @@ func (r *PlaceOrderRequest) OrderType(orderType OrderType) *PlaceOrderRequest { return r } -func (r *PlaceOrderRequest) getParameters() map[string]interface{} { +func (r *PlaceOrderRequest) getParameters() (map[string]interface{}, error) { payload := map[string]interface{}{} payload["symbol"] = r.symbol if r.clientOrderID != nil { payload["clientOid"] = r.clientOrderID + } else { + payload["clientOid"] = uuid.New().String() + } + + if len(r.side) == 0 { + return nil, errors.New("order side is required") } payload["side"] = r.side @@ -140,11 +272,15 @@ func (r *PlaceOrderRequest) getParameters() map[string]interface{} { payload["timeInForce"] = r.timeInForce } - return payload + return payload, nil } func (r *PlaceOrderRequest) Do(ctx context.Context) (*OrderResponse, error) { - payload := r.getParameters() + payload, err := r.getParameters() + if err != nil { + return nil, err + } + req, err := r.client.newAuthenticatedRequest("POST", "/api/v1/orders", nil, payload) if err != nil { return nil, err @@ -165,6 +301,10 @@ func (r *PlaceOrderRequest) Do(ctx context.Context) (*OrderResponse, error) { return nil, err } + if orderResponse.Data == nil { + return nil, errors.New("api error: [" + orderResponse.Code + "] " + orderResponse.Message) + } + return orderResponse.Data, nil } @@ -189,8 +329,8 @@ type CancelOrderResponse struct { CancelledOrderIDs []string `json:"cancelledOrderIds,omitempty"` // used when using client order id for canceling order - CancelledOrderId string `json:"cancelledOrderId,omitempty"` - ClientOrderID string `json:"clientOid,omitempty"` + CancelledOrderId string `json:"cancelledOrderId,omitempty"` + ClientOrderID string `json:"clientOid,omitempty"` } func (r *CancelOrderRequest) Do(ctx context.Context) (*CancelOrderResponse, error) { @@ -236,7 +376,6 @@ type CancelAllOrderRequest struct { // tradeType string } - func (r *CancelAllOrderRequest) Symbol(symbol string) *CancelAllOrderRequest { r.symbol = &symbol return r @@ -254,8 +393,8 @@ func (r *CancelAllOrderRequest) Do(ctx context.Context) (*CancelOrderResponse, e } var orderResponse struct { - Code string `json:"code"` - Message string `json:"msg"` + Code string `json:"code"` + Message string `json:"msg"` Data *CancelOrderResponse `json:"data"` } @@ -273,7 +412,7 @@ type BatchPlaceOrderRequest struct { client *RestClient symbol string - reqs []*PlaceOrderRequest + reqs []*PlaceOrderRequest } func (r *BatchPlaceOrderRequest) Symbol(symbol string) *BatchPlaceOrderRequest { @@ -289,12 +428,16 @@ func (r *BatchPlaceOrderRequest) Add(reqs ...*PlaceOrderRequest) *BatchPlaceOrde func (r *BatchPlaceOrderRequest) Do(ctx context.Context) ([]OrderResponse, error) { var orderList []map[string]interface{} for _, req := range r.reqs { - params := req.getParameters() + params, err := req.getParameters() + if err != nil { + return nil, err + } + orderList = append(orderList, params) } var payload = map[string]interface{}{ - "symbol": r.symbol, + "symbol": r.symbol, "orderList": orderList, } @@ -319,269 +462,3 @@ func (r *BatchPlaceOrderRequest) Do(ctx context.Context) ([]OrderResponse, error return orderResponse.Data, nil } - -type OrderDetails struct { - InstrumentType string `json:"instType"` - InstrumentID string `json:"instId"` - Tag string `json:"tag"` - Price fixedpoint.Value `json:"px"` - Quantity fixedpoint.Value `json:"sz"` - - OrderID string `json:"ordId"` - ClientOrderID string `json:"clOrdId"` - OrderType OrderType `json:"ordType"` - Side SideType `json:"side"` - - // Accumulated fill quantity - FilledQuantity fixedpoint.Value `json:"accFillSz"` - - FeeCurrency string `json:"feeCcy"` - Fee fixedpoint.Value `json:"fee"` - - // trade related fields - LastTradeID string `json:"tradeId,omitempty"` - LastFilledPrice fixedpoint.Value `json:"fillPx"` - LastFilledQuantity fixedpoint.Value `json:"fillSz"` - LastFilledTime types.MillisecondTimestamp `json:"fillTime"` - LastFilledFee fixedpoint.Value `json:"fillFee"` - LastFilledFeeCurrency string `json:"fillFeeCcy"` - - // ExecutionType = liquidity (M = maker or T = taker) - ExecutionType string `json:"execType"` - - // Average filled price. If none is filled, it will return 0. - AveragePrice fixedpoint.Value `json:"avgPx"` - - // Currency = Margin currency - // Only applicable to cross MARGIN orders in Single-currency margin. - Currency string `json:"ccy"` - - // Leverage = from 0.01 to 125. - // Only applicable to MARGIN/FUTURES/SWAP - Leverage fixedpoint.Value `json:"lever"` - - RebateCurrency string `json:"rebateCcy"` - Rebate fixedpoint.Value `json:"rebate"` - - PnL fixedpoint.Value `json:"pnl"` - - UpdateTime types.MillisecondTimestamp `json:"uTime"` - CreationTime types.MillisecondTimestamp `json:"cTime"` - - State OrderState `json:"state"` -} - -type GetOrderDetailsRequest struct { - client *RestClient - - instId string - ordId *string - clOrdId *string -} - -func (r *GetOrderDetailsRequest) InstrumentID(instId string) *GetOrderDetailsRequest { - r.instId = instId - return r -} - -func (r *GetOrderDetailsRequest) OrderID(orderID string) *GetOrderDetailsRequest { - r.ordId = &orderID - return r -} - -func (r *GetOrderDetailsRequest) ClientOrderID(clientOrderID string) *GetOrderDetailsRequest { - r.clOrdId = &clientOrderID - return r -} - -func (r *GetOrderDetailsRequest) QueryParameters() url.Values { - var values = url.Values{} - - values.Add("instId", r.instId) - - if r.ordId != nil { - values.Add("ordId", *r.ordId) - } else if r.clOrdId != nil { - values.Add("clOrdId", *r.clOrdId) - } - - return values -} - -func (r *GetOrderDetailsRequest) Do(ctx context.Context) (*OrderDetails, error) { - params := r.QueryParameters() - req, err := r.client.newAuthenticatedRequest("GET", "/api/v5/trade/order", params, nil) - if err != nil { - return nil, err - } - - response, err := r.client.sendRequest(req) - if err != nil { - return nil, err - } - - var orderResponse struct { - Code string `json:"code"` - Message string `json:"msg"` - Data []OrderDetails `json:"data"` - } - if err := response.DecodeJSON(&orderResponse); err != nil { - return nil, err - } - - if len(orderResponse.Data) == 0 { - return nil, errors.New("order create error") - } - - return &orderResponse.Data[0], nil -} - -type GetPendingOrderRequest struct { - client *RestClient - - instId *string - - instType *InstrumentType - - orderTypes []string - - state *OrderState -} - -func (r *GetPendingOrderRequest) InstrumentID(instId string) *GetPendingOrderRequest { - r.instId = &instId - return r -} - -func (r *GetPendingOrderRequest) InstrumentType(instType InstrumentType) *GetPendingOrderRequest { - r.instType = &instType - return r -} - -func (r *GetPendingOrderRequest) State(state OrderState) *GetPendingOrderRequest { - r.state = &state - return r -} - -func (r *GetPendingOrderRequest) OrderTypes(orderTypes []string) *GetPendingOrderRequest { - r.orderTypes = orderTypes - return r -} - -func (r *GetPendingOrderRequest) AddOrderTypes(orderTypes ...string) *GetPendingOrderRequest { - r.orderTypes = append(r.orderTypes, orderTypes...) - return r -} - -func (r *GetPendingOrderRequest) Parameters() map[string]interface{} { - var payload = map[string]interface{}{} - - if r.instId != nil { - payload["instId"] = r.instId - } - - if r.instType != nil { - payload["instType"] = r.instType - } - - if r.state != nil { - payload["state"] = r.state - } - - if len(r.orderTypes) > 0 { - payload["ordType"] = strings.Join(r.orderTypes, ",") - } - - return payload -} - -func (r *GetPendingOrderRequest) Do(ctx context.Context) ([]OrderDetails, error) { - payload := r.Parameters() - req, err := r.client.newAuthenticatedRequest("GET", "/api/v5/trade/orders-pending", nil, payload) - if err != nil { - return nil, err - } - - response, err := r.client.sendRequest(req) - if err != nil { - return nil, err - } - - var orderResponse struct { - Code string `json:"code"` - Message string `json:"msg"` - Data []OrderDetails `json:"data"` - } - if err := response.DecodeJSON(&orderResponse); err != nil { - return nil, err - } - - return orderResponse.Data, nil -} - -type GetTransactionDetailsRequest struct { - client *RestClient - - instType *InstrumentType - - instId *string - - ordId *string -} - -func (r *GetTransactionDetailsRequest) InstrumentType(instType InstrumentType) *GetTransactionDetailsRequest { - r.instType = &instType - return r -} - -func (r *GetTransactionDetailsRequest) InstrumentID(instId string) *GetTransactionDetailsRequest { - r.instId = &instId - return r -} - -func (r *GetTransactionDetailsRequest) OrderID(orderID string) *GetTransactionDetailsRequest { - r.ordId = &orderID - return r -} - -func (r *GetTransactionDetailsRequest) Parameters() map[string]interface{} { - var payload = map[string]interface{}{} - - if r.instType != nil { - payload["instType"] = r.instType - } - - if r.instId != nil { - payload["instId"] = r.instId - } - - if r.ordId != nil { - payload["ordId"] = r.ordId - } - - return payload -} - -func (r *GetTransactionDetailsRequest) Do(ctx context.Context) ([]OrderDetails, error) { - payload := r.Parameters() - req, err := r.client.newAuthenticatedRequest("GET", "/api/v5/trade/fills", nil, payload) - if err != nil { - return nil, err - } - - response, err := r.client.sendRequest(req) - if err != nil { - return nil, err - } - - var orderResponse struct { - Code string `json:"code"` - Message string `json:"msg"` - Data []OrderDetails `json:"data"` - } - if err := response.DecodeJSON(&orderResponse); err != nil { - return nil, err - } - - return orderResponse.Data, nil -}