From 802d265d56a15e888443a00c922c952b3aebd14d Mon Sep 17 00:00:00 2001 From: David Ackroyd Date: Mon, 18 May 2020 15:42:50 +1000 Subject: [PATCH] Binance: Update NewOrder API (#507) Including new fields for request/response and allowing test orders. Signed-off-by: David Ackroyd --- exchanges/binance/binance.go | 34 ++++++++++++++------ exchanges/binance/binance_test.go | 41 +++++++++++++++++++++++++ exchanges/binance/binance_types.go | 18 ++++++----- exchanges/mock/recording.go | 20 ++++++++++-- testdata/http_mock/binance/binance.json | 24 +++++++++++++++ 5 files changed, 117 insertions(+), 20 deletions(-) diff --git a/exchanges/binance/binance.go b/exchanges/binance/binance.go index 35d82b77..1b77d635 100644 --- a/exchanges/binance/binance.go +++ b/exchanges/binance/binance.go @@ -321,14 +321,35 @@ func (b *Binance) GetBestPrice(symbol string) (BestPrice, error) { // NewOrder sends a new order to Binance func (b *Binance) NewOrder(o *NewOrderRequest) (NewOrderResponse, error) { var resp NewOrderResponse + if err := b.newOrder(newOrder, o, &resp); err != nil { + return resp, err + } - path := b.API.Endpoints.URL + newOrder + if resp.Code != 0 { + return resp, errors.New(resp.Msg) + } + + return resp, nil +} + +// NewOrderTest sends a new test order to Binance +func (b *Binance) NewOrderTest(o *NewOrderRequest) error { + var resp NewOrderResponse + return b.newOrder(newOrderTest, o, &resp) +} + +func (b *Binance) newOrder(api string, o *NewOrderRequest, resp *NewOrderResponse) error { + path := b.API.Endpoints.URL + api params := url.Values{} params.Set("symbol", o.Symbol) params.Set("side", o.Side) params.Set("type", string(o.TradeType)) - params.Set("quantity", strconv.FormatFloat(o.Quantity, 'f', -1, 64)) + if o.QuoteOrderQty > 0 { + params.Set("quoteOrderQty", strconv.FormatFloat(o.QuoteOrderQty, 'f', -1, 64)) + } else { + params.Set("quantity", strconv.FormatFloat(o.Quantity, 'f', -1, 64)) + } if o.TradeType == BinanceRequestParamsOrderLimit { params.Set("price", strconv.FormatFloat(o.Price, 'f', -1, 64)) } @@ -352,14 +373,7 @@ func (b *Binance) NewOrder(o *NewOrderRequest) (NewOrderResponse, error) { params.Set("newOrderRespType", o.NewOrderRespType) } - if err := b.SendAuthHTTPRequest(http.MethodPost, path, params, limitOrder, &resp); err != nil { - return resp, err - } - - if resp.Code != 0 { - return resp, errors.New(resp.Msg) - } - return resp, nil + return b.SendAuthHTTPRequest(http.MethodPost, path, params, limitOrder, resp) } // CancelExistingOrder sends a cancel order to Binance diff --git a/exchanges/binance/binance_test.go b/exchanges/binance/binance_test.go index 57181163..5ba80732 100644 --- a/exchanges/binance/binance_test.go +++ b/exchanges/binance/binance_test.go @@ -353,6 +353,47 @@ func TestGetOrderHistory(t *testing.T) { } } +func TestNewOrderTest(t *testing.T) { + t.Parallel() + + req := &NewOrderRequest{ + Symbol: "LTCBTC", + Side: order.Buy.String(), + TradeType: BinanceRequestParamsOrderLimit, + Price: 0.0025, + Quantity: 100000, + TimeInForce: BinanceRequestParamsTimeGTC, + } + + err := b.NewOrderTest(req) + switch { + case areTestAPIKeysSet() && err != nil: + t.Error("NewOrderTest() error", err) + case !areTestAPIKeysSet() && err == nil && !mockTests: + t.Error("NewOrderTest() expecting an error when no keys are set") + case mockTests && err != nil: + t.Error("Mock NewOrderTest() error", err) + } + + req = &NewOrderRequest{ + Symbol: "LTCBTC", + Side: order.Sell.String(), + TradeType: BinanceRequestParamsOrderMarket, + Price: 0.0045, + QuoteOrderQty: 10, + } + + err = b.NewOrderTest(req) + switch { + case areTestAPIKeysSet() && err != nil: + t.Error("NewOrderTest() error", err) + case !areTestAPIKeysSet() && err == nil && !mockTests: + t.Error("NewOrderTest() expecting an error when no keys are set") + case mockTests && err != nil: + t.Error("Mock NewOrderTest() error", err) + } +} + // Any tests below this line have the ability to impact your orders on the exchange. Enable canManipulateRealOrders to run them // ----------------------------------------------------------------------------------------------------------------------------- diff --git a/exchanges/binance/binance_types.go b/exchanges/binance/binance_types.go index c9a6e825..54d9a15b 100644 --- a/exchanges/binance/binance_types.go +++ b/exchanges/binance/binance_types.go @@ -281,8 +281,10 @@ type NewOrderRequest struct { // TimeInForce specifies how long the order remains in effect. // Examples are (Good Till Cancel (GTC), Immediate or Cancel (IOC) and Fill Or Kill (FOK)) TimeInForce RequestParamsTimeForceType - // Quantity - Quantity float64 + // Quantity is the total base qty spent or received in an order. + Quantity float64 + // QuoteOrderQty is the total quote qty spent or received in a MARKET order. + QuoteOrderQty float64 Price float64 NewClientOrderID string StopPrice float64 // Used with STOP_LOSS, STOP_LOSS_LIMIT, TAKE_PROFIT, and TAKE_PROFIT_LIMIT orders. @@ -301,11 +303,13 @@ type NewOrderResponse struct { Price float64 `json:"price,string"` OrigQty float64 `json:"origQty,string"` ExecutedQty float64 `json:"executedQty,string"` - Status string `json:"status"` - TimeInForce string `json:"timeInForce"` - Type string `json:"type"` - Side string `json:"side"` - Fills []struct { + // The cumulative amount of the quote that has been spent (with a BUY order) or received (with a SELL order). + CumulativeQuoteQty float64 `json:"cummulativeQuoteQty,string"` + Status string `json:"status"` + TimeInForce string `json:"timeInForce"` + Type string `json:"type"` + Side string `json:"side"` + Fills []struct { Price float64 `json:"price,string"` Qty float64 `json:"qty,string"` Commission float64 `json:"commission,string"` diff --git a/exchanges/mock/recording.go b/exchanges/mock/recording.go index 5dff9b45..3bb21087 100644 --- a/exchanges/mock/recording.go +++ b/exchanges/mock/recording.go @@ -148,9 +148,6 @@ func HTTPRecord(res *http.Response, service string, respContents []byte) error { case http.MethodPost: for i := range mockResponses { cType, ok := mockResponses[i].Headers[contentType] - if !ok { - return errors.New("cannot find content type within mock responses") - } jCType := strings.Join(cType, "") var found bool @@ -190,7 +187,24 @@ func HTTPRecord(res *http.Response, service string, respContents []byte) error { mockResponses = append(mockResponses[:i], mockResponses[i+1:]...) found = true } + case "": + if !ok { + // Assume query params are used + mockQuery, urlErr := url.ParseQuery(mockResponses[i].QueryString) + if urlErr != nil { + return urlErr + } + if MatchURLVals(mockQuery, res.Request.URL.Query()) { + // if found will delete instance and overwrite with new data + mockResponses = append(mockResponses[:i], mockResponses[i+1:]...) + found = true + } + + break + } + + fallthrough default: return fmt.Errorf("unhandled content type %s", jCType) } diff --git a/testdata/http_mock/binance/binance.json b/testdata/http_mock/binance/binance.json index ea360a32..11b6ba0b 100644 --- a/testdata/http_mock/binance/binance.json +++ b/testdata/http_mock/binance/binance.json @@ -49951,6 +49951,30 @@ } ] }, + "/api/v3/order/test": { + "POST": [ + { + "data": null, + "queryString": "price=0.0025\u0026quantity=100000\u0026recvWindow=5000\u0026side=BUY\u0026signature=1ddcd1b138325e4b72f045d56e719dc83963648001be999ca23ec88b94b1d900\u0026symbol=LTCBTC\u0026timeInForce=GTC\u0026timestamp=1589766515000\u0026type=LIMIT", + "bodyParams": "", + "headers": { + "X-Mbx-Apikey": [ + "" + ] + } + }, + { + "data": null, + "queryString": "quoteOrderQty=10\u0026recvWindow=5000\u0026side=SELL\u0026signature=f7d34dc6a9d6181adc3cd90f269a78d978610818b4d0b454ec95777194212aae\u0026symbol=LTCBTC\u0026timestamp=1589766516000\u0026type=MARKET", + "bodyParams": "", + "headers": { + "X-Mbx-Apikey": [ + "" + ] + } + } + ] + }, "/api/v3/ticker/24hr": { "GET": [ {