diff --git a/pkg/exchange/max/convert.go b/pkg/exchange/max/convert.go index a43a95723..08903f1c4 100644 --- a/pkg/exchange/max/convert.go +++ b/pkg/exchange/max/convert.go @@ -204,7 +204,7 @@ func toGlobalTrade(t max.Trade) (*types.Trade, error) { var side = toGlobalSideType(t.Side) // trade time - mts := time.Unix(0, t.CreatedAtMilliSeconds*int64(time.Millisecond)) + mts := t.CreatedAtMilliSeconds price, err := fixedpoint.NewFromString(t.Price) if err != nil { diff --git a/pkg/exchange/max/exchange.go b/pkg/exchange/max/exchange.go index 671cd860b..d4359ee2a 100644 --- a/pkg/exchange/max/exchange.go +++ b/pkg/exchange/max/exchange.go @@ -876,7 +876,7 @@ func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *type return nil, err } - req := e.client.TradeService.NewPrivateTradeRequest() + req := e.client.TradeService.NewGetPrivateTradeRequest() req.Market(toLocalSymbol(symbol)) if options.Limit > 0 { @@ -899,11 +899,6 @@ func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *type return nil, err } - // ensure everything is sorted ascending - sort.Slice(maxTrades, func(i, j int) bool { - return maxTrades[i].CreatedAtMilliSeconds < maxTrades[j].CreatedAtMilliSeconds - }) - for _, t := range maxTrades { localTrade, err := toGlobalTrade(t) if err != nil { @@ -914,6 +909,9 @@ func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *type trades = append(trades, *localTrade) } + // ensure everything is sorted ascending + trades = types.SortTradesAscending(trades) + return trades, nil } diff --git a/pkg/exchange/max/maxapi/get_private_trades_request_requestgen.go b/pkg/exchange/max/maxapi/get_private_trades_request_requestgen.go new file mode 100644 index 000000000..1951d196d --- /dev/null +++ b/pkg/exchange/max/maxapi/get_private_trades_request_requestgen.go @@ -0,0 +1,242 @@ +// Code generated by "requestgen -method GET -url v2/trades/my -type GetPrivateTradesRequest -responseType []Trade"; DO NOT EDIT. + +package max + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" + "strconv" + "time" +) + +func (p *GetPrivateTradesRequest) Market(market string) *GetPrivateTradesRequest { + p.market = market + return p +} + +func (p *GetPrivateTradesRequest) Timestamp(timestamp time.Time) *GetPrivateTradesRequest { + p.timestamp = ×tamp + return p +} + +func (p *GetPrivateTradesRequest) From(from int64) *GetPrivateTradesRequest { + p.from = &from + return p +} + +func (p *GetPrivateTradesRequest) To(to int64) *GetPrivateTradesRequest { + p.to = &to + return p +} + +func (p *GetPrivateTradesRequest) OrderBy(orderBy string) *GetPrivateTradesRequest { + p.orderBy = &orderBy + return p +} + +func (p *GetPrivateTradesRequest) Pagination(pagination bool) *GetPrivateTradesRequest { + p.pagination = &pagination + return p +} + +func (p *GetPrivateTradesRequest) Limit(limit int64) *GetPrivateTradesRequest { + p.limit = &limit + return p +} + +func (p *GetPrivateTradesRequest) Offset(offset int64) *GetPrivateTradesRequest { + p.offset = &offset + return p +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (p *GetPrivateTradesRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for k, v := range params { + query.Add(k, fmt.Sprintf("%v", v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (p *GetPrivateTradesRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check market field -> json key market + market := p.market + + // assign parameter of market + params["market"] = market + // check timestamp field -> json key timestamp + if p.timestamp != nil { + timestamp := *p.timestamp + + // assign parameter of timestamp + // convert time.Time to seconds time stamp + params["timestamp"] = strconv.FormatInt(timestamp.Unix(), 10) + } else { + } + // check from field -> json key from + if p.from != nil { + from := *p.from + + // assign parameter of from + params["from"] = from + } else { + } + // check to field -> json key to + if p.to != nil { + to := *p.to + + // assign parameter of to + params["to"] = to + } else { + } + // check orderBy field -> json key order_by + if p.orderBy != nil { + orderBy := *p.orderBy + + // assign parameter of orderBy + params["order_by"] = orderBy + } else { + } + // check pagination field -> json key pagination + if p.pagination != nil { + pagination := *p.pagination + + // assign parameter of pagination + params["pagination"] = pagination + } else { + } + // check limit field -> json key limit + if p.limit != nil { + limit := *p.limit + + // assign parameter of limit + params["limit"] = limit + } else { + } + // check offset field -> json key offset + if p.offset != nil { + offset := *p.offset + + // assign parameter of offset + params["offset"] = offset + } else { + } + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (p *GetPrivateTradesRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := p.GetParameters() + if err != nil { + return query, err + } + + for k, v := range params { + if p.isVarSlice(v) { + p.iterateSlice(v, func(it interface{}) { + query.Add(k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(k, fmt.Sprintf("%v", v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (p *GetPrivateTradesRequest) GetParametersJSON() ([]byte, error) { + params, err := p.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (p *GetPrivateTradesRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (p *GetPrivateTradesRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for k, v := range slugs { + needleRE := regexp.MustCompile(":" + k + "\\b") + url = needleRE.ReplaceAllString(url, v) + } + + return url +} + +func (p *GetPrivateTradesRequest) iterateSlice(slice interface{}, f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for i := 0; i < sliceValue.Len(); i++ { + it := sliceValue.Index(i).Interface() + f(it) + } +} + +func (p *GetPrivateTradesRequest) isVarSlice(v interface{}) bool { + rt := reflect.TypeOf(v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (p *GetPrivateTradesRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := p.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for k, v := range params { + slugs[k] = fmt.Sprintf("%v", v) + } + + return slugs, nil +} + +func (p *GetPrivateTradesRequest) Do(ctx context.Context) ([]Trade, error) { + + // empty params for GET operation + var params interface{} + query, err := p.GetParametersQuery() + if err != nil { + return nil, err + } + + apiURL := "v2/trades/my" + + req, err := p.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := p.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse []Trade + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + return apiResponse, nil +} diff --git a/pkg/exchange/max/maxapi/trade.go b/pkg/exchange/max/maxapi/trade.go index 98a63c20c..093ce2e39 100644 --- a/pkg/exchange/max/maxapi/trade.go +++ b/pkg/exchange/max/maxapi/trade.go @@ -1,11 +1,16 @@ package max +//go:generate -command GetRequest requestgen -method GET +//go:generate -command PostRequest requestgen -method POST + import ( - "context" "net/url" "strconv" + "time" - "github.com/pkg/errors" + "github.com/c9s/requestgen" + + "github.com/c9s/bbgo/pkg/types" ) type MarkerInfo struct { @@ -30,7 +35,7 @@ type Trade struct { Market string `json:"market" db:"market"` MarketName string `json:"market_name"` CreatedAt int64 `json:"created_at"` - CreatedAtMilliSeconds int64 `json:"created_at_in_ms"` + CreatedAtMilliSeconds types.MillisecondTimestamp `json:"created_at_in_ms"` Side string `json:"side" db:"side"` OrderID uint64 `json:"order_id"` Fee string `json:"fee" db:"fee"` // float number as string @@ -111,8 +116,8 @@ func (options *QueryTradeOptions) Params() url.Values { return params } -func (s *TradeService) NewPrivateTradeRequest() *PrivateTradeRequest { - return &PrivateTradeRequest{client: s.client} +func (s *TradeService) NewGetPrivateTradeRequest() *GetPrivateTradesRequest { + return &GetPrivateTradesRequest{client: s.client} } type PrivateRequestParams struct { @@ -120,120 +125,27 @@ type PrivateRequestParams struct { Path string `json:"path"` } -type PrivateTradeRequest struct { - client *RestClient +//go:generate GetRequest -url "v2/trades/my" -type GetPrivateTradesRequest -responseType []Trade +type GetPrivateTradesRequest struct { + client requestgen.AuthenticatedAPIClient - market *string + market string `param:"market"` - // Timestamp is the seconds elapsed since Unix epoch, set to return trades executed before the time only - timestamp *int64 + // timestamp is the seconds elapsed since Unix epoch, set to return trades executed before the time only + timestamp *time.Time `param:"timestamp,seconds"` // From field is a trade id, set ot return trades created after the trade - from *int64 + from *int64 `param:"from"` // To field trade id, set to return trades created before the trade - to *int64 + to *int64 `param:"to"` - orderBy *string + orderBy *string `param:"order_by"` - pagination *bool + pagination *bool `param:"pagination"` - limit *int64 + limit *int64 `param:"limit"` - offset *int64 -} - -func (r *PrivateTradeRequest) Market(market string) *PrivateTradeRequest { - r.market = &market - return r -} - -func (r *PrivateTradeRequest) From(from int64) *PrivateTradeRequest { - r.from = &from - return r -} - -func (r *PrivateTradeRequest) Timestamp(t int64) *PrivateTradeRequest { - r.timestamp = &t - return r -} - -func (r *PrivateTradeRequest) To(to int64) *PrivateTradeRequest { - r.to = &to - return r -} - -func (r *PrivateTradeRequest) Limit(limit int64) *PrivateTradeRequest { - r.limit = &limit - return r -} - -func (r *PrivateTradeRequest) Offset(offset int64) *PrivateTradeRequest { - r.offset = &offset - return r -} - -func (r *PrivateTradeRequest) Pagination(p bool) *PrivateTradeRequest { - r.pagination = &p - return r -} - -func (r *PrivateTradeRequest) OrderBy(orderBy string) *PrivateTradeRequest { - r.orderBy = &orderBy - return r -} - -func (r *PrivateTradeRequest) Do(ctx context.Context) (trades []Trade, err error) { - if r.market == nil { - return nil, errors.New("parameter market is mandatory") - } - - payload := map[string]interface{}{ - "market": r.market, - } - - if r.timestamp != nil { - payload["timestamp"] = r.timestamp - } - - if r.from != nil { - payload["from"] = r.from - } - - if r.to != nil { - payload["to"] = r.to - } - - if r.orderBy != nil { - payload["order_by"] = r.orderBy - } - - if r.pagination != nil { - payload["pagination"] = r.pagination - } - - if r.limit != nil { - payload["limit"] = r.limit - } - - if r.offset != nil { - payload["offset"] = r.offset - } - - req, err := r.client.newAuthenticatedRequest(context.Background(), "GET", "v2/trades/my", nil, payload, nil) - if err != nil { - return trades, err - } - - response, err := r.client.SendRequest(req) - if err != nil { - return trades, err - } - - if err := response.DecodeJSON(&trades); err != nil { - return trades, err - } - - return trades, err + offset *int64 `param:"offset"` } diff --git a/pkg/exchange/max/maxapi/trade_test.go b/pkg/exchange/max/maxapi/trade_test.go new file mode 100644 index 000000000..8287ee05f --- /dev/null +++ b/pkg/exchange/max/maxapi/trade_test.go @@ -0,0 +1,52 @@ +package max + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestTradeService(t *testing.T) { + key, secret, ok := integrationTestConfigured(t, "MAX") + if !ok { + t.SkipNow() + } + + ctx := context.Background() + + client := NewRestClient(ProductionAPIURL) + client.Auth(key, secret) + + t.Run("default timestamp", func(t *testing.T) { + req := client.TradeService.NewGetPrivateTradeRequest() + until := time.Now().AddDate(0, -6, 0) + + trades, err := req.Market("btcusdt"). + Timestamp(until). + Do(ctx) + if assert.NoError(t, err) { + assert.NotEmptyf(t, trades, "got %d trades", len(trades)) + for _, td := range trades { + t.Logf("trade: %+v", td) + assert.True(t, td.CreatedAtMilliSeconds.Time().Before(until)) + } + } + }) + + t.Run("desc and pagination = false", func(t *testing.T) { + req := client.TradeService.NewGetPrivateTradeRequest() + trades, err := req.Market("btcusdt"). + Pagination(false). + OrderBy("asc"). + Do(ctx) + + if assert.NoError(t, err) { + assert.NotEmptyf(t, trades, "got %d trades", len(trades)) + for _, td := range trades { + t.Logf("trade: %+v", td) + } + } + }) +}