diff --git a/v2/client.go b/v2/client.go index fc4f5514..ba51a0cd 100644 --- a/v2/client.go +++ b/v2/client.go @@ -20,6 +20,7 @@ import ( "github.com/adshao/go-binance/v2/common" "github.com/adshao/go-binance/v2/delivery" "github.com/adshao/go-binance/v2/futures" + "github.com/adshao/go-binance/v2/options" ) // SideType define side type of order @@ -309,6 +310,11 @@ func NewDeliveryClient(apiKey, secretKey string) *delivery.Client { return delivery.NewClient(apiKey, secretKey) } +// NewOptionsClient initialize client for options API +func NewOptionsClient(apiKey, secretKey string) *options.Client { + return options.NewClient(apiKey, secretKey) +} + type doFunc func(req *http.Request) (*http.Response, error) // Client define API client diff --git a/v2/options/client.go b/v2/options/client.go new file mode 100644 index 00000000..318a0b97 --- /dev/null +++ b/v2/options/client.go @@ -0,0 +1,420 @@ +package options + +import ( + "bytes" + "context" + "crypto/hmac" + "crypto/sha256" + "crypto/tls" + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + "net/url" + "os" + "time" + + "github.com/adshao/go-binance/v2/common" + "github.com/bitly/go-simplejson" +) + +// SideType define side type of order +type SideType string + +// PositionSideType define position side type of order +type PositionSideType string + +// OptionSideType define option side type of order +type OptionSideType string + +// OrderType define order type +type OrderType string + +// TimeInForceType define time in force type of order +type TimeInForceType string + +// NewOrderRespType define response JSON verbosity +type NewOrderRespType string + +// OrderExecutionType define order execution type +type OrderExecutionType string + +// OrderStatusType define order status type +type OrderStatusType string + +// SymbolType define symbol type +type SymbolType string + +// SymbolStatusType define symbol status type +type SymbolStatusType string + +// SymbolFilterType define symbol filter type +type SymbolFilterType string + +// SideEffectType define side effect type for orders +type SideEffectType string + +// WorkingType define working type +type WorkingType string + +// MarginType define margin type +type MarginType string + +// ContractType define contract type +type ContractType string + +// UserDataEventType define user data event type +type UserDataEventType string + +// UserDataEventReasonType define reason type for user data event +type UserDataEventReasonType string + +// ForceOrderCloseType define reason type for force order +type ForceOrderCloseType string + +// Endpoints +const ( + baseApiMainUrl = "https://eapi.binance.com" + baseApiTestnetUrl = "https://testnet.binancefuture.com" +) + +// Global enums +const ( + SideTypeBuy SideType = "BUY" + SideTypeSell SideType = "SELL" + + PositionSideTypeBoth PositionSideType = "BOTH" + PositionSideTypeLong PositionSideType = "LONG" + PositionSideTypeShort PositionSideType = "SHORT" + + OptionSideTypeCall OptionSideType = "CALL" + OptionSideTypePut OptionSideType = "PUT" + + OrderTypeLimit OrderType = "LIMIT" + OrderTypeMarket OrderType = "MARKET" + OrderTypeStop OrderType = "STOP" + OrderTypeStopMarket OrderType = "STOP_MARKET" + OrderTypeTakeProfit OrderType = "TAKE_PROFIT" + OrderTypeTakeProfitMarket OrderType = "TAKE_PROFIT_MARKET" + OrderTypeTrailingStopMarket OrderType = "TRAILING_STOP_MARKET" + + TimeInForceTypeGTC TimeInForceType = "GTC" // Good Till Cancel + TimeInForceTypeIOC TimeInForceType = "IOC" // Immediate or Cancel + TimeInForceTypeFOK TimeInForceType = "FOK" // Fill or Kill + TimeInForceTypeGTX TimeInForceType = "GTX" // Good Till Crossing (Post Only) + + NewOrderRespTypeACK NewOrderRespType = "ACK" + NewOrderRespTypeRESULT NewOrderRespType = "RESULT" + + OrderExecutionTypeNew OrderExecutionType = "NEW" + OrderExecutionTypePartialFill OrderExecutionType = "PARTIAL_FILL" + OrderExecutionTypeFill OrderExecutionType = "FILL" + OrderExecutionTypeCanceled OrderExecutionType = "CANCELED" + OrderExecutionTypeCalculated OrderExecutionType = "CALCULATED" + OrderExecutionTypeExpired OrderExecutionType = "EXPIRED" + OrderExecutionTypeTrade OrderExecutionType = "TRADE" + + OrderStatusTypeNew OrderStatusType = "NEW" + OrderStatusTypePartiallyFilled OrderStatusType = "PARTIALLY_FILLED" + OrderStatusTypeFilled OrderStatusType = "FILLED" + OrderStatusTypeCanceled OrderStatusType = "CANCELED" + OrderStatusTypeRejected OrderStatusType = "REJECTED" + OrderStatusTypeExpired OrderStatusType = "EXPIRED" + OrderStatusTypeNewInsurance OrderStatusType = "NEW_INSURANCE" + OrderStatusTypeNewADL OrderStatusType = "NEW_ADL" + OrderStatusTypeAccepted OrderStatusType = "ACCEPTED" + + SymbolTypeFuture SymbolType = "FUTURE" + + WorkingTypeMarkPrice WorkingType = "MARK_PRICE" + WorkingTypeContractPrice WorkingType = "CONTRACT_PRICE" + + SymbolStatusTypePreTrading SymbolStatusType = "PRE_TRADING" + SymbolStatusTypeTrading SymbolStatusType = "TRADING" + SymbolStatusTypePostTrading SymbolStatusType = "POST_TRADING" + SymbolStatusTypeEndOfDay SymbolStatusType = "END_OF_DAY" + SymbolStatusTypeHalt SymbolStatusType = "HALT" + SymbolStatusTypeAuctionMatch SymbolStatusType = "AUCTION_MATCH" + SymbolStatusTypeBreak SymbolStatusType = "BREAK" + + SymbolFilterTypeLotSize SymbolFilterType = "LOT_SIZE" + SymbolFilterTypePrice SymbolFilterType = "PRICE_FILTER" + SymbolFilterTypePercentPrice SymbolFilterType = "PERCENT_PRICE" + SymbolFilterTypeMarketLotSize SymbolFilterType = "MARKET_LOT_SIZE" + SymbolFilterTypeMaxNumOrders SymbolFilterType = "MAX_NUM_ORDERS" + SymbolFilterTypeMaxNumAlgoOrders SymbolFilterType = "MAX_NUM_ALGO_ORDERS" + SymbolFilterTypeMinNotional SymbolFilterType = "MIN_NOTIONAL" + + SideEffectTypeNoSideEffect SideEffectType = "NO_SIDE_EFFECT" + SideEffectTypeMarginBuy SideEffectType = "MARGIN_BUY" + SideEffectTypeAutoRepay SideEffectType = "AUTO_REPAY" + + MarginTypeIsolated MarginType = "ISOLATED" + MarginTypeCrossed MarginType = "CROSSED" + + ContractTypePerpetual ContractType = "PERPETUAL" + + UserDataEventTypeListenKeyExpired UserDataEventType = "listenKeyExpired" + UserDataEventTypeMarginCall UserDataEventType = "MARGIN_CALL" + UserDataEventTypeAccountUpdate UserDataEventType = "ACCOUNT_UPDATE" + UserDataEventTypeOrderTradeUpdate UserDataEventType = "ORDER_TRADE_UPDATE" + UserDataEventTypeAccountConfigUpdate UserDataEventType = "ACCOUNT_CONFIG_UPDATE" + + UserDataEventReasonTypeDeposit UserDataEventReasonType = "DEPOSIT" + UserDataEventReasonTypeWithdraw UserDataEventReasonType = "WITHDRAW" + UserDataEventReasonTypeOrder UserDataEventReasonType = "ORDER" + UserDataEventReasonTypeFundingFee UserDataEventReasonType = "FUNDING_FEE" + UserDataEventReasonTypeWithdrawReject UserDataEventReasonType = "WITHDRAW_REJECT" + UserDataEventReasonTypeAdjustment UserDataEventReasonType = "ADJUSTMENT" + UserDataEventReasonTypeInsuranceClear UserDataEventReasonType = "INSURANCE_CLEAR" + UserDataEventReasonTypeAdminDeposit UserDataEventReasonType = "ADMIN_DEPOSIT" + UserDataEventReasonTypeAdminWithdraw UserDataEventReasonType = "ADMIN_WITHDRAW" + UserDataEventReasonTypeMarginTransfer UserDataEventReasonType = "MARGIN_TRANSFER" + UserDataEventReasonTypeMarginTypeChange UserDataEventReasonType = "MARGIN_TYPE_CHANGE" + UserDataEventReasonTypeAssetTransfer UserDataEventReasonType = "ASSET_TRANSFER" + UserDataEventReasonTypeOptionsPremiumFee UserDataEventReasonType = "OPTIONS_PREMIUM_FEE" + UserDataEventReasonTypeOptionsSettleProfit UserDataEventReasonType = "OPTIONS_SETTLE_PROFIT" + + ForceOrderCloseTypeLiquidation ForceOrderCloseType = "LIQUIDATION" + ForceOrderCloseTypeADL ForceOrderCloseType = "ADL" + + timestampKey = "timestamp" + signatureKey = "signature" + recvWindowKey = "recvWindow" +) + +func currentTimestamp() int64 { + return int64(time.Nanosecond) * time.Now().UnixNano() / int64(time.Millisecond) +} + +func newJSON(data []byte) (j *simplejson.Json, err error) { + j, err = simplejson.NewJson(data) + if err != nil { + return nil, err + } + return j, nil +} + +// getApiEndpoint return the base endpoint of the WS +func getApiEndpoint() string { + return baseApiMainUrl +} + +// NewClient initialize an API client instance with API key and secret key. +// You should always call this function before using this SDK. +// Services will be created by the form client.NewXXXService(). +func NewClient(apiKey, secretKey string) *Client { + return &Client{ + APIKey: apiKey, + SecretKey: secretKey, + BaseURL: getApiEndpoint(), + UserAgent: "Binance/golang", + HTTPClient: http.DefaultClient, + Logger: log.New(os.Stderr, "Binance-golang ", log.LstdFlags), + } +} + +// NewProxiedClient passing a proxy url +func NewProxiedClient(apiKey, secretKey, proxyUrl string) *Client { + proxy, err := url.Parse(proxyUrl) + if err != nil { + log.Fatal(err) + } + tr := &http.Transport{ + Proxy: http.ProxyURL(proxy), + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + return &Client{ + APIKey: apiKey, + SecretKey: secretKey, + BaseURL: getApiEndpoint(), + UserAgent: "Binance/golang", + HTTPClient: &http.Client{ + Transport: tr, + }, + Logger: log.New(os.Stderr, "Binance-golang ", log.LstdFlags), + } +} + +type doFunc func(req *http.Request) (*http.Response, error) + +// Client define API client +type Client struct { + APIKey string + SecretKey string + BaseURL string + UserAgent string + HTTPClient *http.Client + Debug bool + Logger *log.Logger + TimeOffset int64 + do doFunc +} + +func (c *Client) debug(format string, v ...interface{}) { + if c.Debug { + c.Logger.Printf(format, v...) + } +} + +func (c *Client) parseRequest(r *request, opts ...RequestOption) (err error) { + // set request options from user + for _, opt := range opts { + opt(r) + } + err = r.validate() + if err != nil { + return err + } + + fullURL := fmt.Sprintf("%s%s", c.BaseURL, r.endpoint) + if r.recvWindow > 0 { + r.setParam(recvWindowKey, r.recvWindow) + } + if r.secType == secTypeSigned { + r.setParam(timestampKey, currentTimestamp()-c.TimeOffset) + } + queryString := r.query.Encode() + body := &bytes.Buffer{} + bodyString := r.form.Encode() + header := http.Header{} + if r.header != nil { + header = r.header.Clone() + } + if bodyString != "" { + header.Set("Content-Type", "application/x-www-form-urlencoded") + body = bytes.NewBufferString(bodyString) + } + if r.secType == secTypeAPIKey || r.secType == secTypeSigned { + header.Set("X-MBX-APIKEY", c.APIKey) + } + + if r.secType == secTypeSigned { + raw := fmt.Sprintf("%s%s", queryString, bodyString) + mac := hmac.New(sha256.New, []byte(c.SecretKey)) + _, err = mac.Write([]byte(raw)) + if err != nil { + return err + } + v := url.Values{} + v.Set(signatureKey, fmt.Sprintf("%x", (mac.Sum(nil)))) + if queryString == "" { + queryString = v.Encode() + } else { + queryString = fmt.Sprintf("%s&%s", queryString, v.Encode()) + } + } + if queryString != "" { + fullURL = fmt.Sprintf("%s?%s", fullURL, queryString) + } + c.debug("full url: %s, body: %s", fullURL, bodyString) + + r.fullURL = fullURL + r.header = header + r.body = body + return nil +} + +func (c *Client) callAPI(ctx context.Context, r *request, opts ...RequestOption) (data []byte, header *http.Header, err error) { + err = c.parseRequest(r, opts...) + if err != nil { + return []byte{}, &http.Header{}, err + } + req, err := http.NewRequest(r.method, r.fullURL, r.body) + if err != nil { + return []byte{}, &http.Header{}, err + } + req = req.WithContext(ctx) + req.Header = r.header + c.debug("request: %#v", req) + f := c.do + if f == nil { + f = c.HTTPClient.Do + } + res, err := f(req) + if err != nil { + return []byte{}, &http.Header{}, err + } + data, err = ioutil.ReadAll(res.Body) + if err != nil { + return []byte{}, &http.Header{}, err + } + defer func() { + cerr := res.Body.Close() + // Only overwrite the retured error if the original error was nil and an + // error occurred while closing the body. + if err == nil && cerr != nil { + err = cerr + } + }() + c.debug("response: %#v", res) + c.debug("response body: %s", string(data)) + c.debug("response status code: %d", res.StatusCode) + + if res.StatusCode >= http.StatusBadRequest { + apiErr := new(common.APIError) + e := json.Unmarshal(data, apiErr) + if e != nil { + c.debug("failed to unmarshal json: %s", e) + } + return nil, &http.Header{}, apiErr + } + return data, &res.Header, nil +} + +// SetApiEndpoint set api Endpoint +func (c *Client) SetApiEndpoint(url string) *Client { + c.BaseURL = url + return c +} + +// NewKlinesService init klines service +func (c *Client) NewKlinesService() *KlinesService { + return &KlinesService{c: c} +} + +// NewDepthService init depth service +func (c *Client) NewDepthService() *DepthService { + return &DepthService{c: c} +} + +// NewExchangeInfoService init exchange info service +func (c *Client) NewExchangeInfoService() *ExchangeInfoService { + return &ExchangeInfoService{c: c} +} + +// NewCreateOrderService init creating order service +func (c *Client) NewCreateOrderService() *CreateOrderService { + return &CreateOrderService{c: c} +} + +// NewListOpenOrdersService init list open orders service +func (c *Client) NewListOpenOrdersService() *ListOpenOrdersService { + return &ListOpenOrdersService{c: c} +} + +// NewGetOrderService init get order service +func (c *Client) NewGetOrderService() *GetOrderService { + return &GetOrderService{c: c} +} + +// NewCancelOrderService init cancel order service +func (c *Client) NewCancelOrderService() *CancelOrderService { + return &CancelOrderService{c: c} +} + +// NewCancelAllOpenOrdersService init cancel all open orders service +func (c *Client) NewCancelAllOpenOrdersService() *CancelAllOpenOrdersService { + return &CancelAllOpenOrdersService{c: c} +} + +// NewCancelMultipleOrdersService init cancel multiple orders service +func (c *Client) NewCancelMultipleOrdersService() *CancelMultiplesOrdersService { + return &CancelMultiplesOrdersService{c: c} +} + +// NewCreateBatchOrdersService init creating batch order service +func (c *Client) NewCreateBatchOrdersService() *CreateBatchOrdersService { + return &CreateBatchOrdersService{c: c} +} diff --git a/v2/options/client_test.go b/v2/options/client_test.go new file mode 100644 index 00000000..f9711cfc --- /dev/null +++ b/v2/options/client_test.go @@ -0,0 +1,144 @@ +package options + +import ( + "bytes" + "context" + "io/ioutil" + "net/http" + "net/url" + + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" +) + +type baseTestSuite struct { + suite.Suite + client *mockedClient + apiKey string + secretKey string +} + +func (s *baseTestSuite) r() *require.Assertions { + return s.Require() +} + +func (s *baseTestSuite) SetupTest() { + s.apiKey = "dummyAPIKey" + s.secretKey = "dummySecretKey" + s.client = newMockedClient(s.apiKey, s.secretKey) +} + +func (s *baseTestSuite) mockDo(data []byte, err error, statusCode ...int) { + s.client.Client.do = s.client.do + code := http.StatusOK + if len(statusCode) > 0 { + code = statusCode[0] + } + s.client.On("do", anyHTTPRequest()).Return(newHTTPResponse(data, code), err) +} + +func (s *baseTestSuite) assertDo() { + s.client.AssertCalled(s.T(), "do", anyHTTPRequest()) +} + +func (s *baseTestSuite) assertReq(f func(r *request)) { + s.client.assertReq = f +} + +func (s *baseTestSuite) assertRequestEqual(e, a *request) { + s.assertURLValuesEqual(e.query, a.query) + s.assertURLValuesEqual(e.form, a.form) +} + +func (s *baseTestSuite) assertURLValuesEqual(e, a url.Values) { + var eKeys, aKeys []string + for k := range e { + eKeys = append(eKeys, k) + } + for k := range a { + aKeys = append(aKeys, k) + } + r := s.r() + r.Len(aKeys, len(eKeys)) + for k := range a { + switch k { + case timestampKey, signatureKey: + r.NotEmpty(a.Get(k)) + continue + } + r.Equal(e.Get(k), a.Get(k), k) + } +} + +func anythingOfType(t string) mock.AnythingOfTypeArgument { + return mock.AnythingOfType(t) +} + +func newContext() context.Context { + return context.Background() +} + +func anyHTTPRequest() mock.AnythingOfTypeArgument { + return anythingOfType("*http.Request") +} + +func newHTTPResponse(data []byte, statusCode int) *http.Response { + return &http.Response{ + Body: ioutil.NopCloser(bytes.NewBuffer(data)), + StatusCode: statusCode, + } +} + +func newRequest() *request { + r := &request{ + query: url.Values{}, + form: url.Values{}, + } + return r +} + +func newSignedRequest() *request { + return newRequest().setParams(params{ + timestampKey: "", + signatureKey: "", + }) +} + +type assertReqFunc func(r *request) + +type mockedClient struct { + mock.Mock + *Client + assertReq assertReqFunc +} + +func newMockedClient(apiKey, secretKey string) *mockedClient { + m := new(mockedClient) + m.Client = NewClient(apiKey, secretKey) + return m +} + +func (m *mockedClient) do(req *http.Request) (*http.Response, error) { + if m.assertReq != nil { + r := newRequest() + r.query = req.URL.Query() + if req.Body != nil { + bs := make([]byte, req.ContentLength) + for { + n, _ := req.Body.Read(bs) + if n == 0 { + break + } + } + form, err := url.ParseQuery(string(bs)) + if err != nil { + panic(err) + } + r.form = form + } + m.assertReq(r) + } + args := m.Called(req) + return args.Get(0).(*http.Response), args.Error(1) +} diff --git a/v2/options/depth_service.go b/v2/options/depth_service.go new file mode 100644 index 00000000..f515cc13 --- /dev/null +++ b/v2/options/depth_service.go @@ -0,0 +1,83 @@ +package options + +import ( + "context" + "net/http" + + "github.com/adshao/go-binance/v2/common" +) + +// DepthService show depth info +type DepthService struct { + c *Client + symbol string + limit *int +} + +// Symbol set symbol +func (s *DepthService) Symbol(symbol string) *DepthService { + s.symbol = symbol + return s +} + +// Limit set limit +func (s *DepthService) Limit(limit int) *DepthService { + s.limit = &limit + return s +} + +// Do send request +func (s *DepthService) Do(ctx context.Context, opts ...RequestOption) (res *DepthResponse, err error) { + r := &request{ + method: http.MethodGet, + endpoint: "/eapi/v1/depth", + } + r.setParam("symbol", s.symbol) + if s.limit != nil { + r.setParam("limit", *s.limit) + } + data, _, err := s.c.callAPI(ctx, r, opts...) + if err != nil { + return nil, err + } + j, err := newJSON(data) + if err != nil { + return nil, err + } + res = new(DepthResponse) + res.TradeTime = j.Get("T").MustInt64() + res.UpdateID = j.Get("u").MustInt64() + bidsLen := len(j.Get("bids").MustArray()) + res.Bids = make([]Bid, bidsLen) + for i := 0; i < bidsLen; i++ { + item := j.Get("bids").GetIndex(i) + res.Bids[i] = Bid{ + Price: item.GetIndex(0).MustString(), + Quantity: item.GetIndex(1).MustString(), + } + } + asksLen := len(j.Get("asks").MustArray()) + res.Asks = make([]Ask, asksLen) + for i := 0; i < asksLen; i++ { + item := j.Get("asks").GetIndex(i) + res.Asks[i] = Ask{ + Price: item.GetIndex(0).MustString(), + Quantity: item.GetIndex(1).MustString(), + } + } + return res, nil +} + +// DepthResponse define depth info with bids and asks +type DepthResponse struct { + TradeTime int64 `json:"T"` + UpdateID int64 `json:"u"` + Bids []Bid `json:"bids"` + Asks []Ask `json:"asks"` +} + +// Ask is a type alias for PriceLevel. +type Ask = common.PriceLevel + +// Bid is a type alias for PriceLevel. +type Bid = common.PriceLevel diff --git a/v2/options/depth_service_test.go b/v2/options/depth_service_test.go new file mode 100644 index 00000000..995557a7 --- /dev/null +++ b/v2/options/depth_service_test.go @@ -0,0 +1,78 @@ +package options + +import ( + "testing" + + "github.com/stretchr/testify/suite" +) + +type depthServiceTestSuite struct { + baseTestSuite +} + +func TestDepthService(t *testing.T) { + suite.Run(t, new(depthServiceTestSuite)) +} + +func (s *depthServiceTestSuite) TestDepth() { + data := []byte(`{ + "T": 1589436922972, + "u": 1027024, + "bids": [ + [ + "4.00000000", + "431.00000000" + ] + ], + "asks": [ + [ + "4.00000200", + "12.00000000" + ] + ] + }`) + s.mockDo(data, nil) + defer s.assertDo() + symbol := "BTC-220722-19000-C" + limit := 3 + s.assertReq(func(r *request) { + e := newRequest().setParam("symbol", symbol). + setParam("limit", limit) + s.assertRequestEqual(e, r) + }) + res, err := s.client.NewDepthService().Symbol(symbol).Limit(limit).Do(newContext()) + s.r().NoError(err) + e := &DepthResponse{ + TradeTime: 1589436922972, + UpdateID: 1027024, + Bids: []Bid{ + { + Price: "4.00000000", + Quantity: "431.00000000", + }, + }, + Asks: []Ask{ + { + Price: "4.00000200", + Quantity: "12.00000000", + }, + }, + } + s.assertDepthResponseEqual(e, res) +} + +func (s *depthServiceTestSuite) assertDepthResponseEqual(e, a *DepthResponse) { + r := s.r() + r.Equal(e.TradeTime, a.TradeTime, "TradeTime") + r.Equal(e.UpdateID, a.UpdateID, "UpdateID") + r.Len(a.Bids, len(e.Bids)) + for i := 0; i < len(a.Bids); i++ { + r.Equal(e.Bids[i].Price, a.Bids[i].Price, "Price") + r.Equal(e.Bids[i].Quantity, a.Bids[i].Quantity, "Quantity") + } + r.Len(a.Asks, len(e.Asks)) + for i := 0; i < len(a.Asks); i++ { + r.Equal(e.Asks[i].Price, a.Asks[i].Price, "Price") + r.Equal(e.Asks[i].Quantity, a.Asks[i].Quantity, "Quantity") + } +} diff --git a/v2/options/exchange_info_service.go b/v2/options/exchange_info_service.go new file mode 100644 index 00000000..f58f4fcb --- /dev/null +++ b/v2/options/exchange_info_service.go @@ -0,0 +1,261 @@ +package options + +import ( + "context" + "encoding/json" + "net/http" + "strconv" +) + +// ExchangeInfoService exchange info service +type ExchangeInfoService struct { + c *Client +} + +// Do send request +func (s *ExchangeInfoService) Do(ctx context.Context, opts ...RequestOption) (res *ExchangeInfo, err error) { + r := &request{ + method: http.MethodGet, + endpoint: "/eapi/v1/exchangeInfo", + secType: secTypeNone, + } + data, _, err := s.c.callAPI(ctx, r, opts...) + if err != nil { + return nil, err + } + res = new(ExchangeInfo) + err = json.Unmarshal(data, res) + if err != nil { + return nil, err + } + + return res, nil +} + +// ExchangeInfo exchange info +type ExchangeInfo struct { + Timezone string `json:"timezone"` + ServerTime int64 `json:"serverTime"` + OptionContracts []OptionContract `json:"optionContracts"` + OptionAssets []OptionAsset `json:"optionAssets"` + OptionSymbols []OptionSymbol `json:"optionSymbols"` + RateLimits []RateLimit `json:"rateLimits"` +} + +// RateLimit struct +type RateLimit struct { + RateLimitType string `json:"rateLimitType"` + Interval string `json:"interval"` + IntervalNum int64 `json:"intervalNum"` + Limit int64 `json:"limit"` +} + +// Option Contract +type OptionContract struct { + Id int64 `json:"id"` + BaseAsset string `json:"baseAsset"` + QuoteAsset string `json:"quoteAsset"` + Underlying string `json:"underlying"` + SettleAsset string `json:"settleAsset"` +} + +// Option Asset +type OptionAsset struct { + Id int64 `json:"id"` + Name string `json:"name"` +} + +// Option Symbol +type OptionSymbol struct { + ContractId int64 `json:"contractId"` + ExpiryDate int64 `json:"expiryDate"` + Filters []map[string]interface{} `json:"filters"` + Id int64 `json:"id"` + Symbol string `json:"symbol"` + Side string `json:"side"` + StrikePrice string `json:"strikePrice"` + Underlying string `json:"underlying"` + Unit int64 `json:"unit"` + MakerFeeRate string `json:"makerFeeRate"` + TakerFeeRate string `json:"takerFeeRate"` + MinQty string `json:"minQty"` + MaxQty string `json:"maxQty"` + InitialMargin string `json:"initialMargin"` + MaintenanceMargin string `json:"maintenanceMargin"` + MinInitialMargin string `json:"minInitialMargin"` + MinMaintenanceMargin string `json:"minMaintenanceMargin"` + PriceScale int `json:"priceScale"` + QuantityScale int `json:"quantityScale"` + QuoteAsset string `json:"quoteAsset"` +} + +// LotSizeFilter define lot size filter of symbol +type LotSizeFilter struct { + MaxQuantity string `json:"maxQty"` + MinQuantity string `json:"minQty"` + StepSize string `json:"stepSize"` +} + +// PriceFilter define price filter of symbol +type PriceFilter struct { + MaxPrice string `json:"maxPrice"` + MinPrice string `json:"minPrice"` + TickSize string `json:"tickSize"` +} + +// PercentPriceFilter define percent price filter of symbol +type PercentPriceFilter struct { + MultiplierDecimal int `json:"multiplierDecimal"` + MultiplierUp string `json:"multiplierUp"` + MultiplierDown string `json:"multiplierDown"` +} + +// MarketLotSizeFilter define market lot size filter of symbol +type MarketLotSizeFilter struct { + MaxQuantity string `json:"maxQty"` + MinQuantity string `json:"minQty"` + StepSize string `json:"stepSize"` +} + +// MaxNumOrdersFilter define max num orders filter of symbol +type MaxNumOrdersFilter struct { + Limit int64 `json:"limit"` +} + +// MaxNumAlgoOrdersFilter define max num algo orders filter of symbol +type MaxNumAlgoOrdersFilter struct { + Limit int64 `json:"limit"` +} + +// MinNotionalFilter define min notional filter of symbol +type MinNotionalFilter struct { + Notional string `json:"notional"` +} + +// LotSizeFilter return lot size filter of symbol +func (s *OptionSymbol) LotSizeFilter() *LotSizeFilter { + for _, filter := range s.Filters { + if filter["filterType"].(string) == string(SymbolFilterTypeLotSize) { + f := &LotSizeFilter{} + if i, ok := filter["maxQty"]; ok { + f.MaxQuantity = i.(string) + } + if i, ok := filter["minQty"]; ok { + f.MinQuantity = i.(string) + } + if i, ok := filter["stepSize"]; ok { + f.StepSize = i.(string) + } + return f + } + } + return nil +} + +// PriceFilter return price filter of symbol +func (s *OptionSymbol) PriceFilter() *PriceFilter { + for _, filter := range s.Filters { + if filter["filterType"].(string) == string(SymbolFilterTypePrice) { + f := &PriceFilter{} + if i, ok := filter["maxPrice"]; ok { + f.MaxPrice = i.(string) + } + if i, ok := filter["minPrice"]; ok { + f.MinPrice = i.(string) + } + if i, ok := filter["tickSize"]; ok { + f.TickSize = i.(string) + } + return f + } + } + return nil +} + +// PercentPriceFilter return percent price filter of symbol +func (s *OptionSymbol) PercentPriceFilter() *PercentPriceFilter { + for _, filter := range s.Filters { + if filter["filterType"].(string) == string(SymbolFilterTypePercentPrice) { + f := &PercentPriceFilter{} + if i, ok := filter["multiplierDecimal"]; ok { + smd, is := i.(string) + if is { + md, _ := strconv.Atoi(smd) + f.MultiplierDecimal = md + } else { + f.MultiplierDecimal = int(i.(float64)) + } + } + if i, ok := filter["multiplierUp"]; ok { + f.MultiplierUp = i.(string) + } + if i, ok := filter["multiplierDown"]; ok { + f.MultiplierDown = i.(string) + } + return f + } + } + return nil +} + +// MarketLotSizeFilter return market lot size filter of symbol +func (s *OptionSymbol) MarketLotSizeFilter() *MarketLotSizeFilter { + for _, filter := range s.Filters { + if filter["filterType"].(string) == string(SymbolFilterTypeMarketLotSize) { + f := &MarketLotSizeFilter{} + if i, ok := filter["maxQty"]; ok { + f.MaxQuantity = i.(string) + } + if i, ok := filter["minQty"]; ok { + f.MinQuantity = i.(string) + } + if i, ok := filter["stepSize"]; ok { + f.StepSize = i.(string) + } + return f + } + } + return nil +} + +// MaxNumOrdersFilter return max num orders filter of symbol +func (s *OptionSymbol) MaxNumOrdersFilter() *MaxNumOrdersFilter { + for _, filter := range s.Filters { + if filter["filterType"].(string) == string(SymbolFilterTypeMaxNumOrders) { + f := &MaxNumOrdersFilter{} + if i, ok := filter["limit"]; ok { + f.Limit = int64(i.(float64)) + } + return f + } + } + return nil +} + +// MaxNumAlgoOrdersFilter return max num orders filter of symbol +func (s *OptionSymbol) MaxNumAlgoOrdersFilter() *MaxNumAlgoOrdersFilter { + for _, filter := range s.Filters { + if filter["filterType"].(string) == string(SymbolFilterTypeMaxNumAlgoOrders) { + f := &MaxNumAlgoOrdersFilter{} + if i, ok := filter["limit"]; ok { + f.Limit = int64(i.(float64)) + } + return f + } + } + return nil +} + +// MinNotionalFilter return min notional filter of symbol +func (s *OptionSymbol) MinNotionalFilter() *MinNotionalFilter { + for _, filter := range s.Filters { + if filter["filterType"].(string) == string(SymbolFilterTypeMinNotional) { + f := &MinNotionalFilter{} + if i, ok := filter["notional"]; ok { + f.Notional = i.(string) + } + return f + } + } + return nil +} diff --git a/v2/options/exchange_info_service_test.go b/v2/options/exchange_info_service_test.go new file mode 100644 index 00000000..4367a461 --- /dev/null +++ b/v2/options/exchange_info_service_test.go @@ -0,0 +1,259 @@ +package options + +import ( + "testing" + + "github.com/stretchr/testify/suite" +) + +type exchangeInfoServiceTestSuite struct { + baseTestSuite +} + +func TestExchangeInfoService(t *testing.T) { + suite.Run(t, new(exchangeInfoServiceTestSuite)) +} + +func (s *exchangeInfoServiceTestSuite) TestExchangeInfo() { + data := []byte(`{ + "timezone": "UTC", + "serverTime": 1592387337630, + "optionContracts": [ + { + "id": 1, + "baseAsset": "BTC", + "quoteAsset": "USDT", + "underlying": "BTCUSDT", + "settleAsset": "USDT" + } + ], + "optionAssets": [ + { + "id": 1, + "name": "USDT" + } + ], + "optionSymbols": [ + { + "contractId": 2, + "expiryDate": 1660521600000, + "filters": [ + { + "filterType": "PRICE_FILTER", + "minPrice": "0.02", + "maxPrice": "80000.01", + "tickSize": "0.01" + }, + { + "filterType": "LOT_SIZE", + "minQty": "0.01", + "maxQty": "100", + "stepSize": "0.01" + } + ], + "id": 17, + "symbol": "BTC-220815-50000-C", + "side": "CALL", + "strikePrice": "50000", + "underlying": "BTCUSDT", + "unit": 1, + "makerFeeRate": "0.0002", + "takerFeeRate": "0.0002", + "minQty": "0.01", + "maxQty": "100", + "initialMargin": "0.15", + "maintenanceMargin": "0.075", + "minInitialMargin": "0.1", + "minMaintenanceMargin": "0.05", + "priceScale": 2, + "quantityScale": 2, + "quoteAsset": "USDT" + } + ], + "rateLimits": [ + { + "rateLimitType": "REQUEST_WEIGHT", + "interval": "MINUTE", + "intervalNum": 1, + "limit": 2400 + }, + { + "rateLimitType": "ORDERS", + "interval": "MINUTE", + "intervalNum": 1, + "limit": 1200 + }, + { + "rateLimitType": "ORDERS", + "interval": "SECOND", + "intervalNum": 10, + "limit": 300 + } + ] + } + `) + s.mockDo(data, nil) + defer s.assertDo() + s.assertReq(func(r *request) { + e := newRequest() + s.assertRequestEqual(e, r) + }) + res, err := s.client.NewExchangeInfoService().Do(newContext()) + s.r().NoError(err) + ei := &ExchangeInfo{ + Timezone: "UTC", + ServerTime: 1592387337630, + OptionContracts: []OptionContract{ + { + Id: 1, + BaseAsset: "BTC", + QuoteAsset: "USDT", + Underlying: "BTCUSDT", + SettleAsset: "USDT", + }, + }, + OptionAssets: []OptionAsset{ + { + Id: 1, + Name: "USDT", + }, + }, + OptionSymbols: []OptionSymbol{ + { + ContractId: 2, + ExpiryDate: 1660521600000, + Filters: []map[string]interface{}{ + {"filterType": "PRICE_FILTER", "minPrice": "0.02", "maxPrice": "80000.01", "tickSize": "0.01"}, + {"filterType": "LOT_SIZE", "minQty": "0.01", "maxQty": "100", "stepSize": "0.01"}, + }, + Id: 17, + Symbol: "BTC-220815-50000-C", + Side: "CALL", + StrikePrice: "50000", + Underlying: "BTCUSDT", + Unit: 1, + MakerFeeRate: "0.0002", + TakerFeeRate: "0.0002", + MinQty: "0.01", + MaxQty: "100", + InitialMargin: "0.15", + MaintenanceMargin: "0.075", + MinInitialMargin: "0.1", + MinMaintenanceMargin: "0.05", + PriceScale: 2, + QuantityScale: 2, + QuoteAsset: "USDT", + }, + }, + RateLimits: []RateLimit{ + {RateLimitType: "REQUEST_WEIGHT", Interval: "MINUTE", IntervalNum: 1, Limit: 2400}, + {RateLimitType: "ORDERS", Interval: "MINUTE", IntervalNum: 1, Limit: 1200}, + {RateLimitType: "ORDERS", Interval: "SECOND", IntervalNum: 10, Limit: 300}, + }, + } + s.assertExchangeInfoEqual(ei, res) + s.r().Len(ei.OptionSymbols[0].Filters, 2, "Filters") + ePriceFilter := &PriceFilter{ + MinPrice: "0.02", + MaxPrice: "80000.01", + TickSize: "0.01", + } + s.assertPriceFilterEqual(ePriceFilter, res.OptionSymbols[0].PriceFilter()) + eLotSizeFilter := &LotSizeFilter{ + MinQuantity: "0.01", + MaxQuantity: "100", + StepSize: "0.01", + } + s.assertLotSizeFilterEqual(eLotSizeFilter, res.OptionSymbols[0].LotSizeFilter()) +} + +func (s *exchangeInfoServiceTestSuite) assertExchangeInfoEqual(e, a *ExchangeInfo) { + r := s.r() + + r.Equal(e.Timezone, a.Timezone, "Timezone") + r.Equal(e.ServerTime, a.ServerTime, "ServerTime") + + r.Len(a.OptionContracts, len(e.OptionContracts), "OptionContracts") + for i := range a.OptionContracts { + r.Equal(e.OptionContracts[i].Id, a.OptionContracts[i].Id, "Id") + r.Equal(e.OptionContracts[i].BaseAsset, a.OptionContracts[i].BaseAsset, "BaseAsset") + r.Equal(e.OptionContracts[i].QuoteAsset, a.OptionContracts[i].QuoteAsset, "QuoteAsset") + r.Equal(e.OptionContracts[i].Underlying, a.OptionContracts[i].Underlying, "Underlying") + r.Equal(e.OptionContracts[i].SettleAsset, a.OptionContracts[i].SettleAsset, "SettleAsset") + } + + r.Len(a.OptionAssets, len(e.OptionAssets), "OptionAssets") + for i := range a.OptionAssets { + r.Equal(e.OptionAssets[i].Id, a.OptionAssets[i].Id, "Id") + r.Equal(e.OptionAssets[i].Name, a.OptionAssets[i].Name, "Name") + } + + r.Len(a.OptionSymbols, len(e.OptionSymbols), "Symbols") + for i := range a.OptionSymbols { + r.Equal(e.OptionSymbols[i].ContractId, a.OptionSymbols[i].ContractId, "ContractId") + r.Equal(e.OptionSymbols[i].ExpiryDate, a.OptionSymbols[i].ExpiryDate, "ExpiryDate") + r.Equal(e.OptionSymbols[i].Id, a.OptionSymbols[i].Id, "Id") + r.Equal(e.OptionSymbols[i].Symbol, a.OptionSymbols[i].Symbol, "Symbol") + r.Equal(e.OptionSymbols[i].Side, a.OptionSymbols[i].Side, "Side") + r.Equal(e.OptionSymbols[i].StrikePrice, a.OptionSymbols[i].StrikePrice, "StrikePrice") + r.Equal(e.OptionSymbols[i].Underlying, a.OptionSymbols[i].Underlying, "Underlying") + r.Equal(e.OptionSymbols[i].Unit, a.OptionSymbols[i].Unit, "Unit") + r.Equal(e.OptionSymbols[i].MakerFeeRate, a.OptionSymbols[i].MakerFeeRate, "MakerFeeRate") + r.Equal(e.OptionSymbols[i].TakerFeeRate, a.OptionSymbols[i].TakerFeeRate, "TakerFeeRate") + r.Equal(e.OptionSymbols[i].MinQty, a.OptionSymbols[i].MinQty, "MinQty") + r.Equal(e.OptionSymbols[i].MaxQty, a.OptionSymbols[i].MaxQty, "MaxQty") + r.Equal(e.OptionSymbols[i].InitialMargin, a.OptionSymbols[i].InitialMargin, "InitialMargin") + r.Equal(e.OptionSymbols[i].MaintenanceMargin, a.OptionSymbols[i].MaintenanceMargin, "MaintenanceMargin") + r.Equal(e.OptionSymbols[i].MinInitialMargin, a.OptionSymbols[i].MinInitialMargin, "MinInitialMargin") + r.Equal(e.OptionSymbols[i].MinMaintenanceMargin, a.OptionSymbols[i].MinMaintenanceMargin, "MinMaintenanceMargin") + r.Equal(e.OptionSymbols[i].PriceScale, a.OptionSymbols[i].PriceScale, "PriceScale") + r.Equal(e.OptionSymbols[i].QuantityScale, a.OptionSymbols[i].QuantityScale, "QuantityScale") + r.Equal(e.OptionSymbols[i].QuoteAsset, a.OptionSymbols[i].QuoteAsset, "QuoteAsset") + } + + r.Len(a.RateLimits, len(e.RateLimits), "RateLimits") + for i := range a.RateLimits { + r.Equal(e.RateLimits[i].RateLimitType, a.RateLimits[i].RateLimitType, "RateLimitType") + r.Equal(e.RateLimits[i].Limit, a.RateLimits[i].Limit, "Limit") + r.Equal(e.RateLimits[i].Interval, a.RateLimits[i].Interval, "Interval") + r.Equal(e.RateLimits[i].IntervalNum, a.RateLimits[i].IntervalNum, "IntervalNum") + } +} + +func (s *exchangeInfoServiceTestSuite) assertLotSizeFilterEqual(e, a *LotSizeFilter) { + r := s.r() + r.Equal(e.MaxQuantity, a.MaxQuantity, "MaxQuantity") + r.Equal(e.MinQuantity, a.MinQuantity, "MinQuantity") + r.Equal(e.StepSize, a.StepSize, "StepSize") +} + +func (s *exchangeInfoServiceTestSuite) assertPriceFilterEqual(e, a *PriceFilter) { + r := s.r() + r.Equal(e.MaxPrice, a.MaxPrice, "MaxPrice") + r.Equal(e.MinPrice, a.MinPrice, "MinPrice") + r.Equal(e.TickSize, a.TickSize, "TickSize") +} + +func (s *exchangeInfoServiceTestSuite) assertPercentPriceFilterEqual(e, a *PercentPriceFilter) { + r := s.r() + r.Equal(e.MultiplierDecimal, a.MultiplierDecimal, "MultiplierDecimal") + r.Equal(e.MultiplierUp, a.MultiplierUp, "MultiplierUp") + r.Equal(e.MultiplierDown, a.MultiplierDown, "MultiplierDown") +} + +func (s *exchangeInfoServiceTestSuite) assertMarketLotSizeFilterEqual(e, a *MarketLotSizeFilter) { + r := s.r() + r.Equal(e.MaxQuantity, a.MaxQuantity, "MaxQuantity") + r.Equal(e.MinQuantity, a.MinQuantity, "MinQuantity") + r.Equal(e.StepSize, a.StepSize, "StepSize") +} + +func (s *exchangeInfoServiceTestSuite) assertMaxNumOrdersFilterEqual(e, a *MaxNumOrdersFilter) { + r := s.r() + r.Equal(e.Limit, a.Limit, "Limit") +} + +func (s *exchangeInfoServiceTestSuite) assertMaxNumAlgoOrdersFilterEqual(e, a *MaxNumAlgoOrdersFilter) { + r := s.r() + r.Equal(e.Limit, a.Limit, "Limit") +} diff --git a/v2/options/kline_service.go b/v2/options/kline_service.go new file mode 100644 index 00000000..c01b45ac --- /dev/null +++ b/v2/options/kline_service.go @@ -0,0 +1,114 @@ +package options + +import ( + "context" + "fmt" + "net/http" +) + +// KlinesService list klines +type KlinesService struct { + c *Client + symbol string + interval string + limit *int + startTime *int64 + endTime *int64 +} + +// Symbol set symbol +func (s *KlinesService) Symbol(symbol string) *KlinesService { + s.symbol = symbol + return s +} + +// Interval set interval +func (s *KlinesService) Interval(interval string) *KlinesService { + s.interval = interval + return s +} + +// Limit set limit +func (s *KlinesService) Limit(limit int) *KlinesService { + s.limit = &limit + return s +} + +// StartTime set startTime +func (s *KlinesService) StartTime(startTime int64) *KlinesService { + s.startTime = &startTime + return s +} + +// EndTime set endTime +func (s *KlinesService) EndTime(endTime int64) *KlinesService { + s.endTime = &endTime + return s +} + +// Do send request +func (s *KlinesService) Do(ctx context.Context, opts ...RequestOption) (res []*Kline, err error) { + r := &request{ + method: http.MethodGet, + endpoint: "/eapi/v1/klines", + } + r.setParam("symbol", s.symbol) + r.setParam("interval", s.interval) + if s.limit != nil { + r.setParam("limit", *s.limit) + } + if s.startTime != nil { + r.setParam("startTime", *s.startTime) + } + if s.endTime != nil { + r.setParam("endTime", *s.endTime) + } + data, _, err := s.c.callAPI(ctx, r, opts...) + if err != nil { + return []*Kline{}, err + } + j, err := newJSON(data) + if err != nil { + return []*Kline{}, err + } + num := len(j.MustArray()) + res = make([]*Kline, num) + for i := 0; i < num; i++ { + item := j.GetIndex(i) + if len(item.MustMap()) < 12 { + err = fmt.Errorf("invalid kline response") + return []*Kline{}, err + } + res[i] = &Kline{ + OpenTime: item.Get("openTime").MustInt64(), + Open: item.Get("open").MustString(), + High: item.Get("high").MustString(), + Low: item.Get("low").MustString(), + Close: item.Get("close").MustString(), + CloseTime: item.Get("closeTime").MustInt64(), + Amount: item.Get("amount").MustString(), + TakerAmount: item.Get("takerAmount").MustString(), + Volume: item.Get("volume").MustString(), + TakerVolume: item.Get("takerVolume").MustString(), + Interval: item.Get("interval").MustString(), + TradeCount: item.Get("tradeCount").MustInt64(), + } + } + return res, nil +} + +// Kline define kline info +type Kline struct { + OpenTime int64 `json:"openTime"` + Open string `json:"open"` + High string `json:"high"` + Low string `json:"low"` + Close string `json:"close"` + CloseTime int64 `json:"closeTime"` + Amount string `json:"amount"` + TakerAmount string `json:"takerAmount"` + Volume string `json:"volume"` + TakerVolume string `json:"takerVolume"` + Interval string `json:"interval"` + TradeCount int64 `json:"tradeCount"` +} diff --git a/v2/options/kline_service_test.go b/v2/options/kline_service_test.go new file mode 100644 index 00000000..c8977e4d --- /dev/null +++ b/v2/options/kline_service_test.go @@ -0,0 +1,131 @@ +package options + +import ( + "testing" + + "github.com/stretchr/testify/suite" +) + +type klineServiceTestSuite struct { + baseTestSuite +} + +func TestKlineService(t *testing.T) { + suite.Run(t, new(klineServiceTestSuite)) +} + +func (s *klineServiceTestSuite) TestKlines() { + /* + OpenTime int64 `json:"openTime"` + Open string `json:"open"` + High string `json:"high"` + Low string `json:"low"` + Close string `json:"close"` + CloseTime int64 `json:"closeTime"` + Amount string `json:"amount"` + TakerAmount string `json:"takerAmount"` + Volume string `json:"volume"` + TakerVolume string `json:"takerVolume"` + Interval string `json:"interval"` + TradeCount int64 `json:"tradeCount"` + */ + //amount:2.35 close:235 closeTime:1677931200000 high:235 interval:4h low:235 open:235 openTime:1677916800000 takerAmount:2.35 takerVolume:0.01 tradeCount:1 volume:0.01 + data := []byte(`[ + { + "openTime":1499040000000, + "open":"0.01634790", + "high":"0.80000000", + "low":"0.01575800", + "close":"0.01577100", + "closeTime":1499644799999, + "amount":"34.66", + "takerAmount":"7.6", + "volume":"1.06", + "takerVolume":"1.02", + "interval":"15m", + "tradeCount":14 + }, + { + "openTime":1499040000001, + "open":"0.01634790", + "high":"0.80000000", + "low":"0.01575800", + "close":"0.01577101", + "closeTime":1499644799999, + "amount":"17.15", + "takerAmount":"4.6", + "volume":"0.06", + "takerVolume":"0.02", + "interval":"15m", + "tradeCount":5 + } + ]`) + s.mockDo(data, nil) + defer s.assertDo() + + symbol := "LTCBTC" + interval := "15m" + limit := 10 + startTime := int64(1499040000000) + endTime := int64(1499040000001) + s.assertReq(func(r *request) { + e := newRequest().setParams(params{ + "symbol": symbol, + "interval": interval, + "limit": limit, + "startTime": startTime, + "endTime": endTime, + }) + s.assertRequestEqual(e, r) + }) + klines, err := s.client.NewKlinesService().Symbol(symbol). + Interval(interval).Limit(limit).StartTime(startTime). + EndTime(endTime).Do(newContext()) + s.r().NoError(err) + s.Len(klines, 2) + kline1 := &Kline{ + OpenTime: 1499040000000, + Open: "0.01634790", + High: "0.80000000", + Low: "0.01575800", + Close: "0.01577100", + CloseTime: 1499644799999, + Amount: "34.66", + TakerAmount: "7.6", + Volume: "1.06", + TakerVolume: "1.02", + Interval: "15m", + TradeCount: 14, + } + kline2 := &Kline{ + OpenTime: 1499040000001, + Open: "0.01634790", + High: "0.80000000", + Low: "0.01575800", + Close: "0.01577101", + CloseTime: 1499644799999, + Amount: "17.15", + TakerAmount: "4.6", + Volume: "0.06", + TakerVolume: "0.02", + Interval: "15m", + TradeCount: 5, + } + s.assertKlineEqual(kline1, klines[0]) + s.assertKlineEqual(kline2, klines[1]) +} +func (s *klineServiceTestSuite) assertKlineEqual(e, a *Kline) { + r := s.r() + r.Equal(e.OpenTime, a.OpenTime, "OpenTime") + r.Equal(e.Open, a.Open, "Open") + r.Equal(e.High, a.High, "High") + r.Equal(e.Low, a.Low, "Low") + r.Equal(e.Close, a.Close, "Close") + r.Equal(e.CloseTime, a.CloseTime, "CloseTime") + r.Equal(e.Amount, a.Amount, "Amount") + r.Equal(e.TakerAmount, a.TakerAmount, "TakerAmount") + r.Equal(e.Volume, a.Volume, "Volume") + r.Equal(e.TakerVolume, a.TakerVolume, "TakerVolume") + r.Equal(e.Interval, a.Interval, "Interval") + r.Equal(e.TradeCount, a.TradeCount, "TradeCount") +} diff --git a/v2/options/order_service.go b/v2/options/order_service.go new file mode 100644 index 00000000..5328fdfb --- /dev/null +++ b/v2/options/order_service.go @@ -0,0 +1,619 @@ +package options + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "strings" +) + +// CreateOrderService create order +type CreateOrderService struct { + c *Client + symbol string + side SideType + orderType OrderType + quantity string + price *string + timeInForce *TimeInForceType + reduceOnly *bool + postOnly *bool + newOrderRespType NewOrderRespType + clientOrderID *string + isMmp *bool +} + +// Symbol set symbol +func (s *CreateOrderService) Symbol(symbol string) *CreateOrderService { + s.symbol = symbol + return s +} + +// Side set side +func (s *CreateOrderService) Side(side SideType) *CreateOrderService { + s.side = side + return s +} + +// Type set type +func (s *CreateOrderService) Type(orderType OrderType) *CreateOrderService { + s.orderType = orderType + return s +} + +// TimeInForce set timeInForce +func (s *CreateOrderService) TimeInForce(timeInForce TimeInForceType) *CreateOrderService { + s.timeInForce = &timeInForce + return s +} + +// Quantity set quantity +func (s *CreateOrderService) Quantity(quantity string) *CreateOrderService { + s.quantity = quantity + return s +} + +// ReduceOnly set reduceOnly +func (s *CreateOrderService) ReduceOnly(reduceOnly bool) *CreateOrderService { + s.reduceOnly = &reduceOnly + return s +} + +// PostOnly set postOnly +func (s *CreateOrderService) PostOnly(postOnly bool) *CreateOrderService { + s.postOnly = &postOnly + return s +} + +// Price set price +func (s *CreateOrderService) Price(price string) *CreateOrderService { + s.price = &price + return s +} + +// ClientOrderID set clientOrderID +func (s *CreateOrderService) ClientOrderID(ClientOrderID string) *CreateOrderService { + s.clientOrderID = &ClientOrderID + return s +} + +// NewOrderResponseType set newOrderResponseType +func (s *CreateOrderService) NewOrderResponseType(newOrderResponseType NewOrderRespType) *CreateOrderService { + s.newOrderRespType = newOrderResponseType + return s +} + +// IsMmp set isMmp +func (s *CreateOrderService) IsMmp(isMmp bool) *CreateOrderService { + s.isMmp = &isMmp + return s +} + +func (s *CreateOrderService) createOrder(ctx context.Context, endpoint string, opts ...RequestOption) (data []byte, header *http.Header, err error) { + + r := &request{ + method: http.MethodPost, + endpoint: endpoint, + secType: secTypeSigned, + } + m := params{ + "symbol": s.symbol, + "side": s.side, + "type": s.orderType, + "quantity": s.quantity, + "newOrderRespType": s.newOrderRespType, + } + if s.timeInForce != nil { + m["timeInForce"] = *s.timeInForce + } + if s.reduceOnly != nil { + m["reduceOnly"] = *s.reduceOnly + } + if s.postOnly != nil { + m["postOnly"] = *s.postOnly + } + if s.price != nil { + m["price"] = *s.price + } + if s.clientOrderID != nil { + m["clientOrderId"] = *s.clientOrderID + } + if s.isMmp != nil { + m["isMmp"] = *s.isMmp + } + r.setFormParams(m) + data, header, err = s.c.callAPI(ctx, r, opts...) + if err != nil { + return []byte{}, &http.Header{}, err + } + return data, header, nil +} + +// Do send request +func (s *CreateOrderService) Do(ctx context.Context, opts ...RequestOption) (res *CreateOrderResponse, err error) { + data, header, err := s.createOrder(ctx, "/eapi/v1/order", opts...) + if err != nil { + return nil, err + } + res = new(CreateOrderResponse) + err = json.Unmarshal(data, res) + res.RateLimitOrder10s = header.Get("X-Mbx-Order-Count-10s") + res.RateLimitOrder1m = header.Get("X-Mbx-Order-Count-1m") + + if err != nil { + return nil, err + } + return res, nil +} + +// CreateOrderResponse define create order response +type CreateOrderResponse struct { + OrderID int64 `json:"orderId"` + Symbol string `json:"symbol"` + Price string `json:"price"` + Quantity string `json:"quantity"` + ExecutedQty string `json:"executedQty"` + Fee string `json:"fee"` + Side SideType `json:"side"` + Type OrderType `json:"type"` + TimeInForce TimeInForceType `json:"timeInForce"` + ReduceOnly bool `json:"reduceOnly"` + PostOnly bool `json:"postOnly"` + CreateTime int64 `json:"createTime"` + UpdateTime int64 `json:"updateTime"` + Status OrderStatusType `json:"status"` + AvgPrice string `json:"avgPrice"` + ClientOrderID string `json:"clientOrderId"` + PriceScale int `json:"priceScale"` + QuantityScale int `json:"quantityScale"` + OptionSide OptionSideType `json:"optionSide"` + QuoteAsset string `json:"quoteAsset"` + Mmp bool `json:"mmp"` + RateLimitOrder10s string `json:"rateLimitOrder10s,omitempty"` + RateLimitOrder1m string `json:"rateLimitOrder1m,omitempty"` +} + +// ListOpenOrdersService list opened orders +type ListOpenOrdersService struct { + c *Client + symbol string + orderId *int64 + startTime *int64 + endTime *int64 + limit *int +} + +// Symbol set symbol +func (s *ListOpenOrdersService) Symbol(symbol string) *ListOpenOrdersService { + s.symbol = symbol + return s +} + +// OrderId set orderId +func (s *ListOpenOrdersService) OrderId(orderId int64) *ListOpenOrdersService { + s.orderId = &orderId + return s +} + +// StartTime set startTime +func (s *ListOpenOrdersService) StartTime(startTime int64) *ListOpenOrdersService { + s.startTime = &startTime + return s +} + +// EndTime set endTime +func (s *ListOpenOrdersService) EndTime(endTime int64) *ListOpenOrdersService { + s.endTime = &endTime + return s +} + +// Limit set limit +func (s *ListOpenOrdersService) Limit(limit int) *ListOpenOrdersService { + s.limit = &limit + return s +} + +// Do send request +func (s *ListOpenOrdersService) Do(ctx context.Context, opts ...RequestOption) (res []*Order, err error) { + r := &request{ + method: http.MethodGet, + endpoint: "/eapi/v1/openOrders", + secType: secTypeSigned, + } + if s.symbol != "" { + r.setParam("symbol", s.symbol) + } + if s.orderId != nil { + r.setParam("orderId", s.orderId) + } + if s.startTime != nil { + r.setParam("startTime", s.startTime) + } + if s.endTime != nil { + r.setParam("endTime", s.endTime) + } + if s.limit != nil { + r.setParam("limit", s.limit) + } + data, _, err := s.c.callAPI(ctx, r, opts...) + if err != nil { + return []*Order{}, err + } + res = make([]*Order, 0) + err = json.Unmarshal(data, &res) + if err != nil { + return []*Order{}, err + } + return res, nil +} + +// GetOrderService get an order +type GetOrderService struct { + c *Client + symbol string + orderID *int64 + clientOrderID *string +} + +// Symbol set symbol +func (s *GetOrderService) Symbol(symbol string) *GetOrderService { + s.symbol = symbol + return s +} + +// OrderID set orderID +func (s *GetOrderService) OrderID(orderID int64) *GetOrderService { + s.orderID = &orderID + return s +} + +// ClientOrderID set clientOrderID +func (s *GetOrderService) ClientOrderID(clientOrderID string) *GetOrderService { + s.clientOrderID = &clientOrderID + return s +} + +// Do send request +func (s *GetOrderService) Do(ctx context.Context, opts ...RequestOption) (res *Order, err error) { + r := &request{ + method: http.MethodGet, + endpoint: "/eapi/v1/order", + secType: secTypeSigned, + } + r.setParam("symbol", s.symbol) + if s.orderID != nil { + r.setParam("orderId", *s.orderID) + } + if s.clientOrderID != nil { + r.setParam("clientOrderID", *s.clientOrderID) + } + data, _, err := s.c.callAPI(ctx, r, opts...) + if err != nil { + return nil, err + } + res = new(Order) + err = json.Unmarshal(data, res) + if err != nil { + return nil, err + } + return res, nil +} + +// Order define order info +type Order struct { + OrderID int64 `json:"orderId"` + Symbol string `json:"symbol"` + Price string `json:"price"` + Quantity string `json:"quantity"` + ExecutedQty string `json:"executedQty"` + Fee string `json:"fee"` + Side SideType `json:"side"` + Type OrderType `json:"type"` + TimeInForce TimeInForceType `json:"timeInForce"` + ReduceOnly bool `json:"reduceOnly"` + PostOnly bool `json:"postOnly"` + CreateTime int64 `json:"createTime"` + UpdateTime int64 `json:"updateTime"` + Status OrderStatusType `json:"status"` + AvgPrice string `json:"avgPrice"` + Source string `json:"source"` + ClientOrderID string `json:"clientOrderId"` + PriceScale int `json:"priceScale"` + QuantityScale int `json:"quantityScale"` + OptionSide OptionSideType `json:"optionSide"` + QuoteAsset string `json:"quoteAsset"` + Mmp bool `json:"mmp"` +} + +// CancelOrderService cancel an order +type CancelOrderService struct { + c *Client + symbol string + orderID *int64 + clientOrderID *string +} + +// Symbol set symbol +func (s *CancelOrderService) Symbol(symbol string) *CancelOrderService { + s.symbol = symbol + return s +} + +// OrderID set orderID +func (s *CancelOrderService) OrderID(orderID int64) *CancelOrderService { + s.orderID = &orderID + return s +} + +// ClientOrderID set clientOrderID +func (s *CancelOrderService) ClientOrderID(clientOrderID string) *CancelOrderService { + s.clientOrderID = &clientOrderID + return s +} + +// Do send request +func (s *CancelOrderService) Do(ctx context.Context, opts ...RequestOption) (res *CancelOrderResponse, err error) { + r := &request{ + method: http.MethodDelete, + endpoint: "/eapi/v1/order", + secType: secTypeSigned, + } + r.setFormParam("symbol", s.symbol) + if s.orderID != nil { + r.setFormParam("orderId", *s.orderID) + } + if s.clientOrderID != nil { + r.setFormParam("clientOrderID", *s.clientOrderID) + } + data, _, err := s.c.callAPI(ctx, r, opts...) + if err != nil { + return nil, err + } + res = new(CancelOrderResponse) + err = json.Unmarshal(data, res) + if err != nil { + return nil, err + } + return res, nil +} + +// CancelOrderResponse define response of canceling order +type CancelOrderResponse struct { + OrderID int64 `json:"orderId"` + Symbol string `json:"symbol"` + Price string `json:"price"` + Quantity string `json:"quantity"` + ExecutedQty string `json:"executedQty"` + Fee string `json:"fee"` + Side SideType `json:"side"` + Type OrderType `json:"type"` + TimeInForce TimeInForceType `json:"timeInForce"` + ReduceOnly bool `json:"reduceOnly"` + PostOnly bool `json:"postOnly"` + CreateTime int64 `json:"createTime"` + UpdateTime int64 `json:"updateTime"` + Status OrderStatusType `json:"status"` + AvgPrice string `json:"avgPrice"` + Source string `json:"source"` + ClientOrderID string `json:"clientOrderId"` + PriceScale int `json:"priceScale"` + QuantityScale int `json:"quantityScale"` + OptionSide OptionSideType `json:"optionSide"` + QuoteAsset string `json:"quoteAsset"` + Mmp bool `json:"mmp"` +} + +// CancelAllOpenOrdersService cancel all open orders +type CancelAllOpenOrdersService struct { + c *Client + symbol string +} + +// Symbol set symbol +func (s *CancelAllOpenOrdersService) Symbol(symbol string) *CancelAllOpenOrdersService { + s.symbol = symbol + return s +} + +// Do send request +func (s *CancelAllOpenOrdersService) Do(ctx context.Context, opts ...RequestOption) (err error) { + r := &request{ + method: http.MethodDelete, + endpoint: "/eapi/v1/allOpenOrders", + secType: secTypeSigned, + } + r.setFormParam("symbol", s.symbol) + _, _, err = s.c.callAPI(ctx, r, opts...) + if err != nil { + return err + } + return nil +} + +// CancelMultiplesOrdersService cancel a list of orders +type CancelMultiplesOrdersService struct { + c *Client + symbol string + orderIDList []int64 + clientOrderIDList []string +} + +// CancelSingleOrderResponse define element of response of canceling multiple order +type CancelSingleOrderResponse struct { + OrderID int64 `json:"orderId"` + Symbol string `json:"symbol"` + Price string `json:"price"` + Quantity string `json:"quantity"` + ExecutedQty string `json:"executedQty"` + Fee string `json:"fee"` + Side SideType `json:"side"` + Type OrderType `json:"type"` + TimeInForce TimeInForceType `json:"timeInForce"` + CreateTime int64 `json:"createTime"` + Status OrderStatusType `json:"status"` + AvgPrice string `json:"avgPrice"` + ReduceOnly bool `json:"reduceOnly"` + ClientOrderID string `json:"clientOrderId"` + UpdateTime int64 `json:"updateTime"` +} + +// Symbol set symbol +func (s *CancelMultiplesOrdersService) Symbol(symbol string) *CancelMultiplesOrdersService { + s.symbol = symbol + return s +} + +// OrderID set orderID +func (s *CancelMultiplesOrdersService) OrderIDList(orderIDList []int64) *CancelMultiplesOrdersService { + s.orderIDList = orderIDList + return s +} + +// ClientOrderIDList set clientOrderIDList +func (s *CancelMultiplesOrdersService) ClientOrderIDList(clientOrderIDList []string) *CancelMultiplesOrdersService { + s.clientOrderIDList = clientOrderIDList + return s +} + +// Do send request +func (s *CancelMultiplesOrdersService) Do(ctx context.Context, opts ...RequestOption) (res []*CancelSingleOrderResponse, err error) { + r := &request{ + method: http.MethodDelete, + endpoint: "/eapi/v1/batchOrders", + secType: secTypeSigned, + } + r.setFormParam("symbol", s.symbol) + if s.orderIDList != nil { + // convert a slice of integers to a string e.g. [1 2 3] => "[1,2,3]" + orderIDListString := strings.Join(strings.Fields(fmt.Sprint(s.orderIDList)), ",") + r.setFormParam("orderIdList", orderIDListString) + } + if s.clientOrderIDList != nil { + r.setFormParam("clientOrderIdList", s.clientOrderIDList) + } + data, _, err := s.c.callAPI(ctx, r, opts...) + if err != nil { + return nil, err + } + res = make([]*CancelSingleOrderResponse, 0) + err = json.Unmarshal(data, &res) + if err != nil { + return []*CancelSingleOrderResponse{}, err + } + return res, nil +} + +type CreateBatchOrdersService struct { + c *Client + orders []*CreateOrderService +} + +type CreateBatchOrdersResponse struct { + Orders []*SingleOrder +} + +// SingleOrder define single order info returned by batch create request +type SingleOrder struct { + OrderID int64 `json:"orderId"` + Symbol string `json:"symbol"` + Price string `json:"price"` + Quantity string `json:"quantity"` + Side SideType `json:"side"` + Type OrderType `json:"type"` + ReduceOnly bool `json:"reduceOnly"` + PostOnly bool `json:"postOnly"` + ClientOrderID string `json:"clientOrderId"` + Mmp bool `json:"mmp"` +} + +func (s *CreateBatchOrdersService) OrderList(orders []*CreateOrderService) *CreateBatchOrdersService { + s.orders = orders + return s +} + +func (s *CreateBatchOrdersService) Do(ctx context.Context, opts ...RequestOption) (res *CreateBatchOrdersResponse, err error) { + r := &request{ + method: http.MethodPost, + endpoint: "/eapi/v1/batchOrders", + secType: secTypeSigned, + } + + orders := []params{} + for _, order := range s.orders { + m := params{ + "symbol": order.symbol, + "side": order.side, + "type": order.orderType, + "quantity": order.quantity, + "newOrderRespType": order.newOrderRespType, + } + + if order.timeInForce != nil { + m["timeInForce"] = *order.timeInForce + } + if order.reduceOnly != nil { + m["reduceOnly"] = *order.reduceOnly + } + if order.postOnly != nil { + m["postOnly"] = *order.postOnly + } + if order.price != nil { + m["price"] = *order.price + } + if order.clientOrderID != nil { + m["clientOrderId"] = *order.clientOrderID + } + if order.isMmp != nil { + m["isMmp"] = *order.isMmp + } + orders = append(orders, m) + } + b, err := json.Marshal(orders) + if err != nil { + return &CreateBatchOrdersResponse{}, err + } + m := params{ + "batchOrders": string(b), + } + + r.setFormParams(m) + + data, _, err := s.c.callAPI(ctx, r, opts...) + + if err != nil { + return &CreateBatchOrdersResponse{}, err + } + + rawMessages := make([]*json.RawMessage, 0) + + err = json.Unmarshal(data, &rawMessages) + + if err != nil { + return &CreateBatchOrdersResponse{}, err + } + + batchCreateOrdersResponse := new(CreateBatchOrdersResponse) + + for _, j := range rawMessages { + o := new(SingleOrder) + if err := json.Unmarshal(*j, o); err != nil { + return &CreateBatchOrdersResponse{}, err + } + + /* + TODO: the following 4 lines are copied from futures package, not sure why there is + such condition check and it looks wrong to me. Need to double confirm. + + if o.ClientOrderID != "" { + batchCreateOrdersResponse.Orders = append(batchCreateOrdersResponse.Orders, o) + continue + } + */ + batchCreateOrdersResponse.Orders = append(batchCreateOrdersResponse.Orders, o) + + } + + return batchCreateOrdersResponse, nil + +} diff --git a/v2/options/order_service_test.go b/v2/options/order_service_test.go new file mode 100644 index 00000000..a997902d --- /dev/null +++ b/v2/options/order_service_test.go @@ -0,0 +1,412 @@ +package options + +import ( + "testing" + + "github.com/stretchr/testify/suite" +) + +type baseOrderTestSuite struct { + baseTestSuite +} + +type orderServiceTestSuite struct { + baseOrderTestSuite +} + +func TestOrderService(t *testing.T) { + suite.Run(t, new(orderServiceTestSuite)) +} + +func (s *orderServiceTestSuite) TestCreateOrder() { + data := []byte(`{ + "orderId": 4611875134427365377, + "symbol": "BTC-200730-9000-C", + "price": "100", + "quantity": "1", + "executedQty": "0", + "fee": "0", + "side": "BUY", + "type": "LIMIT", + "timeInForce": "GTC", + "reduceOnly": false, + "postOnly": false, + "createTime": 1592465880683, + "updateTime": 1566818724722, + "status": "ACCEPTED", + "avgPrice": "0", + "clientOrderId": "testOrder", + "priceScale": 2, + "quantityScale": 2, + "optionSide": "CALL", + "quoteAsset": "USDT", + "mmp": false + }`) + s.mockDo(data, nil) + defer s.assertDo() + symbol := "BTC-200730-9000-C" + side := SideTypeBuy + orderType := OrderTypeLimit + quantity := "1" + price := "100" + timeInForce := TimeInForceTypeGTC + reduceOnly := false + postOnly := false + newOrderResponseType := NewOrderRespTypeRESULT + clientOrderID := "testOrder" + isMmp := false + s.assertReq(func(r *request) { + e := newSignedRequest().setFormParams(params{ + "symbol": symbol, + "side": side, + "type": orderType, + "quantity": quantity, + "price": price, + "timeInForce": timeInForce, + "reduceOnly": reduceOnly, + "postOnly": postOnly, + "newOrderRespType": newOrderResponseType, + "clientOrderId": clientOrderID, + "isMmp": isMmp, + }) + s.assertRequestEqual(e, r) + }) + res, err := s.client.NewCreateOrderService().Symbol(symbol).Side(side). + Type(orderType).Quantity(quantity).Price(price).TimeInForce(timeInForce). + ReduceOnly(reduceOnly).PostOnly(postOnly).NewOrderResponseType(newOrderResponseType). + ClientOrderID(clientOrderID).IsMmp(isMmp). + Do(newContext()) + s.r().NoError(err) + e := &CreateOrderResponse{ + OrderID: 4611875134427365377, + Symbol: symbol, + Price: price, + Quantity: quantity, + ExecutedQty: "0", + Fee: "0", + Side: side, + Type: orderType, + TimeInForce: timeInForce, + ReduceOnly: reduceOnly, + PostOnly: postOnly, + CreateTime: 1592465880683, + UpdateTime: 1566818724722, + Status: OrderStatusTypeAccepted, + AvgPrice: "0", + ClientOrderID: clientOrderID, + PriceScale: 2, + QuantityScale: 2, + OptionSide: OptionSideTypeCall, + QuoteAsset: "USDT", + Mmp: isMmp, + } + s.assertCreateOrderResponseEqual(e, res) +} + +func (s *baseOrderTestSuite) assertCreateOrderResponseEqual(e, a *CreateOrderResponse) { + r := s.r() + r.Equal(e.OrderID, a.OrderID, "OrderID") + r.Equal(e.Symbol, a.Symbol, "Symbol") + r.Equal(e.Price, a.Price, "Price") + r.Equal(e.Quantity, a.Quantity, "Quantity") + r.Equal(e.ExecutedQty, a.ExecutedQty, "ExecutedQty") + r.Equal(e.Fee, a.Fee, "Fee") + r.Equal(e.Side, a.Side, "Side") + r.Equal(e.Type, a.Type, "Type") + r.Equal(e.TimeInForce, a.TimeInForce, "TimeInForce") + r.Equal(e.ReduceOnly, a.ReduceOnly, "ReduceOnly") + r.Equal(e.PostOnly, a.PostOnly, "PostOnly") + r.Equal(e.CreateTime, a.CreateTime, "CreateTime") + r.Equal(e.UpdateTime, a.UpdateTime, "UpdateTime") + r.Equal(e.Status, a.Status, "Status") + r.Equal(e.AvgPrice, a.AvgPrice, "AvgPrice") + r.Equal(e.ClientOrderID, a.ClientOrderID, "ClientOrderID") + r.Equal(e.PriceScale, a.PriceScale, "PriceScale") + r.Equal(e.QuantityScale, a.QuantityScale, "QuantityScale") + r.Equal(e.OptionSide, a.OptionSide, "OptionSide") + r.Equal(e.QuoteAsset, a.QuoteAsset, "QuoteAsset") + r.Equal(e.Mmp, a.Mmp, "Mmp") +} + +func (s *orderServiceTestSuite) TestListOpenOrders() { + data := []byte(`[ + { + "orderId": 4611875134427365377, + "symbol": "BTC-200730-9000-C", + "price": "100", + "quantity": "1", + "executedQty": "0", + "fee": "0", + "side": "BUY", + "type": "LIMIT", + "timeInForce": "GTC", + "reduceOnly": false, + "postOnly": false, + "createTime": 1592465880683, + "updateTime": 1592465880683, + "status": "ACCEPTED", + "avgPrice": "0", + "clientOrderId": "", + "priceScale": 2, + "quantityScale": 2, + "optionSide": "CALL", + "quoteAsset": "USDT", + "mmp": false + } + ]`) + s.mockDo(data, nil) + defer s.assertDo() + + symbol := "BTC-200730-9000-C" + recvWindow := int64(1000) + s.assertReq(func(r *request) { + e := newSignedRequest().setParams(params{ + "symbol": symbol, + "recvWindow": recvWindow, + }) + s.assertRequestEqual(e, r) + }) + orders, err := s.client.NewListOpenOrdersService().Symbol(symbol). + Do(newContext(), WithRecvWindow(recvWindow)) + r := s.r() + r.NoError(err) + r.Len(orders, 1) + e := &Order{ + OrderID: 4611875134427365377, + Symbol: symbol, + Price: "100", + Quantity: "1", + ExecutedQty: "0", + Fee: "0", + Side: SideTypeBuy, + Type: OrderTypeLimit, + TimeInForce: TimeInForceTypeGTC, + ReduceOnly: false, + PostOnly: false, + CreateTime: 1592465880683, + UpdateTime: 1592465880683, + Status: OrderStatusTypeAccepted, + AvgPrice: "0", + ClientOrderID: "", + PriceScale: 2, + QuantityScale: 2, + OptionSide: OptionSideTypeCall, + QuoteAsset: "USDT", + Mmp: false, + } + s.assertOrderEqual(e, orders[0]) +} + +func (s *baseOrderTestSuite) assertOrderEqual(e, a *Order) { + r := s.r() + r.Equal(e.OrderID, a.OrderID, "OrderID") + r.Equal(e.Symbol, a.Symbol, "Symbol") + r.Equal(e.Price, a.Price, "Price") + r.Equal(e.Quantity, a.Quantity, "Quantity") + r.Equal(e.ExecutedQty, a.ExecutedQty, "ExecutedQty") + r.Equal(e.Fee, a.Fee, "Fee") + r.Equal(e.Side, a.Side, "Side") + r.Equal(e.Type, a.Type, "Type") + r.Equal(e.TimeInForce, a.TimeInForce, "TimeInForce") + r.Equal(e.ReduceOnly, a.ReduceOnly, "ReduceOnly") + r.Equal(e.PostOnly, a.PostOnly, "PostOnly") + r.Equal(e.CreateTime, a.CreateTime, "CreateTime") + r.Equal(e.UpdateTime, a.UpdateTime, "UpdateTime") + r.Equal(e.Status, a.Status, "Status") + r.Equal(e.AvgPrice, a.AvgPrice, "AvgPrice") + r.Equal(e.ClientOrderID, a.ClientOrderID, "ClientOrderID") + r.Equal(e.PriceScale, a.PriceScale, "PriceScale") + r.Equal(e.QuantityScale, a.QuantityScale, "QuantityScale") + r.Equal(e.OptionSide, a.OptionSide, "OptionSide") + r.Equal(e.QuoteAsset, a.QuoteAsset, "QuoteAsset") + r.Equal(e.Mmp, a.Mmp, "Mmp") +} + +func (s *orderServiceTestSuite) TestGetOrder() { + data := []byte(`{ + "orderId": 4611875134427365377, + "symbol": "BTC-200730-9000-C", + "price": "100", + "quantity": "1", + "executedQty": "0", + "fee": "0", + "side": "BUY", + "type": "LIMIT", + "timeInForce": "GTC", + "reduceOnly": false, + "postOnly": false, + "createTime": 1592465880683, + "updateTime": 1566818724722, + "status": "ACCEPTED", + "avgPrice": "0", + "source": "API", + "clientOrderId": "", + "priceScale": 2, + "quantityScale": 2, + "optionSide": "CALL", + "quoteAsset": "USDT", + "mmp": false + }`) + s.mockDo(data, nil) + defer s.assertDo() + + symbol := "BTC-200730-9000-C" + orderID := int64(4611875134427365377) + clientOrderID := "" + s.assertReq(func(r *request) { + e := newSignedRequest().setParams(params{ + "symbol": symbol, + "orderId": orderID, + "clientOrderId": clientOrderID, + }) + s.assertRequestEqual(e, r) + }) + order, err := s.client.NewGetOrderService().Symbol(symbol). + OrderID(orderID).ClientOrderID(clientOrderID).Do(newContext()) + r := s.r() + r.NoError(err) + e := &Order{ + OrderID: orderID, + Symbol: symbol, + Price: "100", + Quantity: "1", + ExecutedQty: "0", + Fee: "0", + Side: SideTypeBuy, + Type: OrderTypeLimit, + TimeInForce: TimeInForceTypeGTC, + ReduceOnly: false, + PostOnly: false, + CreateTime: 1592465880683, + UpdateTime: 1566818724722, + Status: OrderStatusTypeAccepted, + AvgPrice: "0", + Source: "API", + ClientOrderID: clientOrderID, + PriceScale: 2, + QuantityScale: 2, + OptionSide: OptionSideTypeCall, + QuoteAsset: "USDT", + Mmp: false, + } + s.assertOrderEqual(e, order) +} + +func (s *orderServiceTestSuite) TestCancelOrder() { + data := []byte(`{ + "orderId": 4611875134427365377, + "symbol": "BTC-200730-9000-C", + "price": "100", + "quantity": "1", + "executedQty": "0", + "fee": "0", + "side": "BUY", + "type": "LIMIT", + "timeInForce": "GTC", + "reduceOnly": false, + "postOnly": false, + "CreateTime": 1592465880683, + "updateTime": 1566818724722, + "status": "ACCEPTED", + "avgPrice": "0", + "source": "API", + "clientOrderId": "", + "priceScale": 4, + "quantityScale": 4, + "optionSide": "CALL", + "quoteAsset": "USDT", + "mmp": false + }`) + s.mockDo(data, nil) + defer s.assertDo() + + symbol := "BTC-200730-9000-C" + orderID := int64(4611875134427365377) + clientOrderID := "" + s.assertReq(func(r *request) { + e := newSignedRequest().setFormParams(params{ + "symbol": symbol, + "orderId": orderID, + "clientOrderId": clientOrderID, + }) + s.assertRequestEqual(e, r) + }) + + res, err := s.client.NewCancelOrderService().Symbol(symbol). + OrderID(orderID).ClientOrderID(clientOrderID). + Do(newContext()) + r := s.r() + r.NoError(err) + e := &CancelOrderResponse{ + OrderID: orderID, + Symbol: symbol, + Price: "100", + Quantity: "1", + ExecutedQty: "0", + Fee: "0", + Side: SideTypeBuy, + Type: OrderTypeLimit, + TimeInForce: TimeInForceTypeGTC, + ReduceOnly: false, + PostOnly: false, + CreateTime: 1592465880683, + UpdateTime: 1566818724722, + Status: OrderStatusTypeAccepted, + AvgPrice: "0", + Source: "API", + ClientOrderID: clientOrderID, + PriceScale: 4, + QuantityScale: 4, + OptionSide: OptionSideTypeCall, + QuoteAsset: "USDT", + Mmp: false, + } + s.assertCancelOrderResponseEqual(e, res) +} + +func (s *orderServiceTestSuite) assertCancelOrderResponseEqual(e, a *CancelOrderResponse) { + r := s.r() + r.Equal(e.OrderID, a.OrderID, "OrderID") + r.Equal(e.Symbol, a.Symbol, "Symbol") + r.Equal(e.Price, a.Price, "Price") + r.Equal(e.Quantity, a.Quantity, "Quantity") + r.Equal(e.ExecutedQty, a.ExecutedQty, "ExecutedQty") + r.Equal(e.Fee, a.Fee, "Fee") + r.Equal(e.Side, a.Side, "Side") + r.Equal(e.Type, a.Type, "Type") + r.Equal(e.TimeInForce, a.TimeInForce, "TimeInForce") + r.Equal(e.ReduceOnly, a.ReduceOnly, "ReduceOnly") + r.Equal(e.PostOnly, a.PostOnly, "PostOnly") + r.Equal(e.CreateTime, a.CreateTime, "CreateTime") + r.Equal(e.UpdateTime, a.UpdateTime, "UpdateTime") + r.Equal(e.Status, a.Status, "Status") + r.Equal(e.AvgPrice, a.AvgPrice, "AvgPrice") + r.Equal(e.Source, a.Source, "Source") + r.Equal(e.ClientOrderID, a.ClientOrderID, "ClientOrderID") + r.Equal(e.PriceScale, a.PriceScale, "PriceScale") + r.Equal(e.QuantityScale, a.QuantityScale, "QuantityScale") + r.Equal(e.OptionSide, a.OptionSide, "OptionSide") + r.Equal(e.QuoteAsset, a.QuoteAsset, "QuoteAsset") + r.Equal(e.Mmp, a.Mmp, "Mmp") +} + +func (s *orderServiceTestSuite) TestCancelAllOpenOrders() { + data := []byte(`{ + "code": 0, + "msg": "success" + }`) + s.mockDo(data, nil) + defer s.assertDo() + + symbol := "BTC-200730-9000-C" + s.assertReq(func(r *request) { + e := newSignedRequest().setFormParams(params{ + "symbol": symbol, + }) + s.assertRequestEqual(e, r) + }) + + err := s.client.NewCancelAllOpenOrdersService().Symbol(symbol). + Do(newContext()) + s.r().NoError(err) +} diff --git a/v2/options/request.go b/v2/options/request.go new file mode 100644 index 00000000..c9e96eef --- /dev/null +++ b/v2/options/request.go @@ -0,0 +1,106 @@ +package options + +import ( + "fmt" + "io" + "net/http" + "net/url" +) + +type secType int + +const ( + secTypeNone secType = iota + secTypeAPIKey + secTypeSigned +) + +type params map[string]interface{} + +// request define an API request +type request struct { + method string + endpoint string + query url.Values + form url.Values + recvWindow int64 + secType secType + header http.Header + body io.Reader + fullURL string +} + +// setParam set param with key/value to query string +func (r *request) setParam(key string, value interface{}) *request { + if r.query == nil { + r.query = url.Values{} + } + r.query.Set(key, fmt.Sprintf("%v", value)) + return r +} + +// setParams set params with key/values to query string +func (r *request) setParams(m params) *request { + for k, v := range m { + r.setParam(k, v) + } + return r +} + +// setFormParam set param with key/value to request form body +func (r *request) setFormParam(key string, value interface{}) *request { + if r.form == nil { + r.form = url.Values{} + } + r.form.Set(key, fmt.Sprintf("%v", value)) + return r +} + +// setFormParams set params with key/values to request form body +func (r *request) setFormParams(m params) *request { + for k, v := range m { + r.setFormParam(k, v) + } + return r +} + +func (r *request) validate() (err error) { + if r.query == nil { + r.query = url.Values{} + } + if r.form == nil { + r.form = url.Values{} + } + return nil +} + +// RequestOption define option type for request +type RequestOption func(*request) + +// WithRecvWindow set recvWindow param for the request +func WithRecvWindow(recvWindow int64) RequestOption { + return func(r *request) { + r.recvWindow = recvWindow + } +} + +// WithHeader set or add a header value to the request +func WithHeader(key, value string, replace bool) RequestOption { + return func(r *request) { + if r.header == nil { + r.header = http.Header{} + } + if replace { + r.header.Set(key, value) + } else { + r.header.Add(key, value) + } + } +} + +// WithHeaders set or replace the headers of the request +func WithHeaders(header http.Header) RequestOption { + return func(r *request) { + r.header = header.Clone() + } +}