diff --git a/config/max-margin.yaml b/config/max-margin.yaml index 1ce868952..ad7cc7588 100644 --- a/config/max-margin.yaml +++ b/config/max-margin.yaml @@ -4,6 +4,28 @@ sessions: exchange: max margin: true +sync: + # userDataStream is used to sync the trading data in real-time + # it uses the websocket connection to insert the trades + userDataStream: + trades: false + filledOrders: false + + # since is the start date of your trading data + since: 2019-11-01 + + # sessions is the list of session names you want to sync + # by default, BBGO sync all your available sessions. + sessions: + - max_margin + + # symbols is the list of symbols you want to sync + # by default, BBGO try to guess your symbols by your existing account balances. + symbols: + - BTCUSDT + - ETHUSDT + + exchangeStrategies: - on: max_margin diff --git a/pkg/exchange/max/convert.go b/pkg/exchange/max/convert.go index 07b5537ce..dd1c8b39d 100644 --- a/pkg/exchange/max/convert.go +++ b/pkg/exchange/max/convert.go @@ -168,6 +168,7 @@ func toGlobalOrders(maxOrders []max.Order) (orders []types.Order, err error) { func toGlobalOrder(maxOrder max.Order) (*types.Order, error) { executedVolume := maxOrder.ExecutedVolume remainingVolume := maxOrder.RemainingVolume + isMargin := maxOrder.WalletType == max.WalletTypeMargin return &types.Order{ SubmitOrder: types.SubmitOrder{ @@ -185,52 +186,33 @@ func toGlobalOrder(maxOrder max.Order) (*types.Order, error) { OrderID: maxOrder.ID, Status: toGlobalOrderStatus(maxOrder.State, executedVolume, remainingVolume), ExecutedQuantity: executedVolume, - CreationTime: types.Time(maxOrder.CreatedAtMs.Time()), - UpdateTime: types.Time(maxOrder.CreatedAtMs.Time()), + CreationTime: types.Time(maxOrder.CreatedAt.Time()), + UpdateTime: types.Time(maxOrder.CreatedAt.Time()), + IsMargin: isMargin, + IsIsolated: false, // isolated margin is not supported }, nil } func toGlobalTrade(t max.Trade) (*types.Trade, error) { - // skip trade ID that is the same. however this should not happen - var side = toGlobalSideType(t.Side) - - // trade time - mts := t.CreatedAtMilliSeconds - - price, err := fixedpoint.NewFromString(t.Price) - if err != nil { - return nil, err - } - - quantity, err := fixedpoint.NewFromString(t.Volume) - if err != nil { - return nil, err - } - - quoteQuantity, err := fixedpoint.NewFromString(t.Funds) - if err != nil { - return nil, err - } - - fee, err := fixedpoint.NewFromString(t.Fee) - if err != nil { - return nil, err - } - + isMargin := t.WalletType == max.WalletTypeMargin + side := toGlobalSideType(t.Side) return &types.Trade{ ID: t.ID, OrderID: t.OrderID, - Price: price, + Price: t.Price, Symbol: toGlobalSymbol(t.Market), - Exchange: "max", - Quantity: quantity, + Exchange: types.ExchangeMax, + Quantity: t.Volume, Side: side, IsBuyer: t.IsBuyer(), IsMaker: t.IsMaker(), - Fee: fee, + Fee: t.Fee, FeeCurrency: toGlobalCurrency(t.FeeCurrency), - QuoteQuantity: quoteQuantity, - Time: types.Time(mts), + QuoteQuantity: t.Funds, + Time: types.Time(t.CreatedAt), + IsMargin: isMargin, + IsIsolated: false, + IsFutures: false, }, nil } @@ -297,16 +279,6 @@ func convertWebSocketTrade(t max.TradeUpdate) (*types.Trade, error) { } func convertWebSocketOrderUpdate(u max.OrderUpdate) (*types.Order, error) { - executedVolume, err := fixedpoint.NewFromString(u.ExecutedVolume) - if err != nil { - return nil, err - } - - remainingVolume, err := fixedpoint.NewFromString(u.RemainingVolume) - if err != nil { - return nil, err - } - timeInForce := types.TimeInForceGTC if u.OrderType == max.OrderTypeIOCLimit { timeInForce = types.TimeInForceIOC @@ -318,16 +290,16 @@ func convertWebSocketOrderUpdate(u max.OrderUpdate) (*types.Order, error) { Symbol: toGlobalSymbol(u.Market), Side: toGlobalSideType(u.Side), Type: toGlobalOrderType(u.OrderType), - Quantity: fixedpoint.MustNewFromString(u.Volume), - Price: fixedpoint.MustNewFromString(u.Price), - StopPrice: fixedpoint.MustNewFromString(u.StopPrice), + Quantity: u.Volume, + Price: u.Price, + StopPrice: u.StopPrice, TimeInForce: timeInForce, // MAX only supports GTC GroupID: u.GroupID, }, Exchange: types.ExchangeMax, OrderID: u.ID, - Status: toGlobalOrderStatus(u.State, executedVolume, remainingVolume), - ExecutedQuantity: executedVolume, + Status: toGlobalOrderStatus(u.State, u.ExecutedVolume, u.RemainingVolume), + ExecutedQuantity: u.ExecutedVolume, CreationTime: types.Time(time.Unix(0, u.CreatedAtMs*int64(time.Millisecond))), UpdateTime: types.Time(time.Unix(0, u.CreatedAtMs*int64(time.Millisecond))), }, nil diff --git a/pkg/exchange/max/exchange.go b/pkg/exchange/max/exchange.go index 288db5807..750162069 100644 --- a/pkg/exchange/max/exchange.go +++ b/pkg/exchange/max/exchange.go @@ -773,11 +773,17 @@ func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *type return nil, err } - req := e.client.TradeService.NewGetPrivateTradeRequest() - req.Market(toLocalSymbol(symbol)) + market := toLocalSymbol(symbol) + walletType := maxapi.WalletTypeSpot + if e.MarginSettings.IsMargin { + walletType = maxapi.WalletTypeMargin + } + + req := e.v3order.NewWalletGetTradesRequest(walletType) + req.Market(market) if options.Limit > 0 { - req.Limit(options.Limit) + req.Limit(uint64(options.Limit)) } else { req.Limit(1000) } @@ -785,12 +791,9 @@ func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *type // MAX uses exclusive last trade ID // the timestamp parameter is used for reverse order, we can't use it. if options.LastTradeID > 0 { - req.From(int64(options.LastTradeID)) + req.From(options.LastTradeID) } - // make it compatible with binance, we need the last trade id for the next page. - req.OrderBy("asc") - maxTrades, err := req.Do(ctx) if err != nil { return nil, err diff --git a/pkg/exchange/max/maxapi/get_order_history_request_requestgen.go b/pkg/exchange/max/maxapi/get_order_history_request_requestgen.go deleted file mode 100644 index 7cb15cf0b..000000000 --- a/pkg/exchange/max/maxapi/get_order_history_request_requestgen.go +++ /dev/null @@ -1,174 +0,0 @@ -// Code generated by "requestgen -method GET -url v2/orders/history -type GetOrderHistoryRequest -responseType []Order"; DO NOT EDIT. - -package max - -import ( - "context" - "encoding/json" - "fmt" - "net/url" - "reflect" - "regexp" -) - -func (g *GetOrderHistoryRequest) Market(market string) *GetOrderHistoryRequest { - g.market = market - return g -} - -func (g *GetOrderHistoryRequest) FromID(fromID uint64) *GetOrderHistoryRequest { - g.fromID = &fromID - return g -} - -func (g *GetOrderHistoryRequest) Limit(limit uint) *GetOrderHistoryRequest { - g.limit = &limit - return g -} - -// GetQueryParameters builds and checks the query parameters and returns url.Values -func (g *GetOrderHistoryRequest) 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 (g *GetOrderHistoryRequest) GetParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - // check market field -> json key market - market := g.market - - // assign parameter of market - params["market"] = market - // check fromID field -> json key from_id - if g.fromID != nil { - fromID := *g.fromID - - // assign parameter of fromID - params["from_id"] = fromID - } else { - } - // check limit field -> json key limit - if g.limit != nil { - limit := *g.limit - - // assign parameter of limit - params["limit"] = limit - } else { - } - - return params, nil -} - -// GetParametersQuery converts the parameters from GetParameters into the url.Values format -func (g *GetOrderHistoryRequest) GetParametersQuery() (url.Values, error) { - query := url.Values{} - - params, err := g.GetParameters() - if err != nil { - return query, err - } - - for _k, _v := range params { - if g.isVarSlice(_v) { - g.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 (g *GetOrderHistoryRequest) GetParametersJSON() ([]byte, error) { - params, err := g.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 (g *GetOrderHistoryRequest) GetSlugParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - - return params, nil -} - -func (g *GetOrderHistoryRequest) 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 (g *GetOrderHistoryRequest) 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 (g *GetOrderHistoryRequest) isVarSlice(_v interface{}) bool { - rt := reflect.TypeOf(_v) - switch rt.Kind() { - case reflect.Slice: - return true - } - return false -} - -func (g *GetOrderHistoryRequest) GetSlugsMap() (map[string]string, error) { - slugs := map[string]string{} - params, err := g.GetSlugParameters() - if err != nil { - return slugs, nil - } - - for _k, _v := range params { - slugs[_k] = fmt.Sprintf("%v", _v) - } - - return slugs, nil -} - -func (g *GetOrderHistoryRequest) Do(ctx context.Context) ([]Order, error) { - - // empty params for GET operation - var params interface{} - query, err := g.GetParametersQuery() - if err != nil { - return nil, err - } - - apiURL := "v2/orders/history" - - req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) - if err != nil { - return nil, err - } - - response, err := g.client.SendRequest(req) - if err != nil { - return nil, err - } - - var apiResponse []Order - if err := response.DecodeJSON(&apiResponse); err != nil { - return nil, err - } - return apiResponse, 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 deleted file mode 100644 index 1951d196d..000000000 --- a/pkg/exchange/max/maxapi/get_private_trades_request_requestgen.go +++ /dev/null @@ -1,242 +0,0 @@ -// 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/order.go b/pkg/exchange/max/maxapi/order.go index 6d88ec767..41e47bb24 100644 --- a/pkg/exchange/max/maxapi/order.go +++ b/pkg/exchange/max/maxapi/order.go @@ -6,7 +6,6 @@ package max import ( "context" "net/url" - "time" "github.com/c9s/requestgen" "github.com/pkg/errors" @@ -99,7 +98,7 @@ type SubmitOrder struct { // Order represents one returned order (POST order/GET order/GET orders) on the max platform. type Order struct { ID uint64 `json:"id,omitempty"` - WalletType string `json:"wallet_type,omitempty"` + WalletType WalletType `json:"wallet_type,omitempty"` Side string `json:"side"` OrderType OrderType `json:"ord_type"` Price fixedpoint.Value `json:"price,omitempty"` @@ -113,64 +112,7 @@ type Order struct { TradesCount int64 `json:"trades_count,omitempty"` GroupID uint32 `json:"group_id,omitempty"` ClientOID string `json:"client_oid,omitempty"` - CreatedAt time.Time `json:"-"` - CreatedAtMs types.MillisecondTimestamp `json:"created_at_in_ms,omitempty"` - InsertedAt time.Time `json:"-"` -} - -// Open returns open orders -func (s *OrderService) Closed(market string, options QueryOrderOptions) ([]Order, error) { - req := s.NewGetOrdersRequest() - req.Market(market) - req.State([]OrderState{OrderStateDone, OrderStateCancel}) - - if options.GroupID > 0 { - req.GroupID(uint32(options.GroupID)) - } - if options.Offset > 0 { - req.Offset(options.Offset) - } - if options.Limit > 0 { - req.Limit(options.Limit) - } - - if options.Page > 0 { - req.Page(options.Page) - } - - if len(options.OrderBy) > 0 { - req.OrderBy(options.OrderBy) - } - - return req.Do(context.Background()) -} - -// Open returns open orders -func (s *OrderService) Open(market string, options QueryOrderOptions) ([]Order, error) { - req := s.NewGetOrdersRequest() - req.Market(market) - // state default ot wait and convert - - if options.GroupID > 0 { - req.GroupID(uint32(options.GroupID)) - } - - return req.Do(context.Background()) -} - -//go:generate GetRequest -url "v2/orders/history" -type GetOrderHistoryRequest -responseType []Order -type GetOrderHistoryRequest struct { - client requestgen.AuthenticatedAPIClient - - market string `param:"market"` - fromID *uint64 `param:"from_id"` - limit *uint `param:"limit"` -} - -func (s *OrderService) NewGetOrderHistoryRequest() *GetOrderHistoryRequest { - return &GetOrderHistoryRequest{ - client: s.client, - } + CreatedAt types.MillisecondTimestamp `json:"created_at"` } //go:generate GetRequest -url "v2/orders" -type GetOrdersRequest -responseType []Order diff --git a/pkg/exchange/max/maxapi/order_test.go b/pkg/exchange/max/maxapi/order_test.go index a69d920ba..49d6c6e4c 100644 --- a/pkg/exchange/max/maxapi/order_test.go +++ b/pkg/exchange/max/maxapi/order_test.go @@ -73,24 +73,6 @@ func TestOrderService_GetOrdersRequest_SingleState(t *testing.T) { assert.NotNil(t, orders) } -func TestOrderService_GetOrderHistoryRequest(t *testing.T) { - key, secret, ok := integrationTestConfigured(t, "MAX") - if !ok { - t.SkipNow() - } - - ctx := context.Background() - - client := NewRestClient(ProductionAPIURL) - client.Auth(key, secret) - - req := client.OrderService.NewGetOrderHistoryRequest() - req.Market("btcusdt") - req.FromID(1) - orders, err := req.Do(ctx) - assert.NoError(t, err) - assert.NotNil(t, orders) -} func TestOrderService(t *testing.T) { key, secret, ok := integrationTestConfigured(t, "MAX") diff --git a/pkg/exchange/max/maxapi/trade.go b/pkg/exchange/max/maxapi/trade.go index 093ce2e39..149c91d50 100644 --- a/pkg/exchange/max/maxapi/trade.go +++ b/pkg/exchange/max/maxapi/trade.go @@ -10,6 +10,7 @@ import ( "github.com/c9s/requestgen" + "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" ) @@ -26,21 +27,24 @@ type TradeInfo struct { Ask *MarkerInfo `json:"ask,omitempty"` } +type Liquidity string + // Trade represents one returned trade on the max platform. type Trade struct { - ID uint64 `json:"id" db:"exchange_id"` - Price string `json:"price" db:"price"` - Volume string `json:"volume" db:"volume"` - Funds string `json:"funds"` - Market string `json:"market" db:"market"` - MarketName string `json:"market_name"` - CreatedAt int64 `json:"created_at"` - 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 - FeeCurrency string `json:"fee_currency" db:"fee_currency"` - Info TradeInfo `json:"info,omitempty"` + ID uint64 `json:"id" db:"exchange_id"` + WalletType WalletType `json:"wallet_type,omitempty"` + Price fixedpoint.Value `json:"price"` + Volume fixedpoint.Value `json:"volume"` + Funds fixedpoint.Value `json:"funds"` + Market string `json:"market"` + MarketName string `json:"market_name"` + CreatedAt types.MillisecondTimestamp `json:"created_at"` + Side string `json:"side"` + OrderID uint64 `json:"order_id"` + Fee fixedpoint.Value `json:"fee"` // float number as string + FeeCurrency string `json:"fee_currency"` + Liquidity Liquidity `json:"liquidity"` + Info TradeInfo `json:"info,omitempty"` } func (t Trade) IsBuyer() bool { @@ -148,4 +152,3 @@ type GetPrivateTradesRequest struct { offset *int64 `param:"offset"` } - diff --git a/pkg/exchange/max/maxapi/trade_test.go b/pkg/exchange/max/maxapi/trade_test.go deleted file mode 100644 index 8287ee05f..000000000 --- a/pkg/exchange/max/maxapi/trade_test.go +++ /dev/null @@ -1,52 +0,0 @@ -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) - } - } - }) -} diff --git a/pkg/exchange/max/maxapi/userdata.go b/pkg/exchange/max/maxapi/userdata.go index 5d3978ddd..8fe3e3c5d 100644 --- a/pkg/exchange/max/maxapi/userdata.go +++ b/pkg/exchange/max/maxapi/userdata.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/pkg/errors" + log "github.com/sirupsen/logrus" "github.com/valyala/fastjson" "github.com/c9s/bbgo/pkg/fixedpoint" @@ -23,16 +24,16 @@ type OrderUpdate struct { Side string `json:"sd"` OrderType OrderType `json:"ot"` - Price string `json:"p"` - StopPrice string `json:"sp"` + Price fixedpoint.Value `json:"p"` + StopPrice fixedpoint.Value `json:"sp"` - Volume string `json:"v"` - AveragePrice string `json:"ap"` - State OrderState `json:"S"` - Market string `json:"M"` + Volume fixedpoint.Value `json:"v"` + AveragePrice fixedpoint.Value `json:"ap"` + State OrderState `json:"S"` + Market string `json:"M"` - RemainingVolume string `json:"rv"` - ExecutedVolume string `json:"ev"` + RemainingVolume fixedpoint.Value `json:"rv"` + ExecutedVolume fixedpoint.Value `json:"ev"` TradesCount int64 `json:"tc"` @@ -48,36 +49,20 @@ type OrderUpdateEvent struct { Orders []OrderUpdate `json:"o"` } -func parserOrderUpdate(v *fastjson.Value) OrderUpdate { - return OrderUpdate{ - Event: string(v.GetStringBytes("e")), - ID: v.GetUint64("i"), - Side: string(v.GetStringBytes("sd")), - Market: string(v.GetStringBytes("M")), - OrderType: OrderType(v.GetStringBytes("ot")), - State: OrderState(v.GetStringBytes("S")), - Price: string(v.GetStringBytes("p")), - StopPrice: string(v.GetStringBytes("sp")), - AveragePrice: string(v.GetStringBytes("ap")), - Volume: string(v.GetStringBytes("v")), - RemainingVolume: string(v.GetStringBytes("rv")), - ExecutedVolume: string(v.GetStringBytes("ev")), - TradesCount: v.GetInt64("tc"), - GroupID: uint32(v.GetInt("gi")), - ClientOID: string(v.GetStringBytes("ci")), - CreatedAtMs: v.GetInt64("T"), - UpdateTime: v.GetInt64("TU"), - } -} - func parseOrderUpdateEvent(v *fastjson.Value) *OrderUpdateEvent { var e OrderUpdateEvent e.Event = string(v.GetStringBytes("e")) e.Timestamp = v.GetInt64("T") for _, ov := range v.GetArray("o") { - o := parserOrderUpdate(ov) - e.Orders = append(e.Orders, o) + var o = ov.String() + var u OrderUpdate + if err := json.Unmarshal([]byte(o), &u); err != nil { + log.WithError(err).Error("parse error") + continue + } + + e.Orders = append(e.Orders, u) } return &e @@ -95,8 +80,14 @@ func parserOrderSnapshotEvent(v *fastjson.Value) *OrderSnapshotEvent { e.Timestamp = v.GetInt64("T") for _, ov := range v.GetArray("o") { - o := parserOrderUpdate(ov) - e.Orders = append(e.Orders, o) + var o = ov.String() + var u OrderUpdate + if err := json.Unmarshal([]byte(o), &u); err != nil { + log.WithError(err).Error("parse error") + continue + } + + e.Orders = append(e.Orders, u) } return &e diff --git a/pkg/exchange/max/maxapi/v3/order.go b/pkg/exchange/max/maxapi/v3/order.go index ed70e7eb6..d0581c000 100644 --- a/pkg/exchange/max/maxapi/v3/order.go +++ b/pkg/exchange/max/maxapi/v3/order.go @@ -5,6 +5,8 @@ package v3 //go:generate -command DeleteRequest requestgen -method DELETE import ( + "time" + "github.com/c9s/requestgen" maxapi "github.com/c9s/bbgo/pkg/exchange/max/maxapi" @@ -35,6 +37,10 @@ func (s *OrderService) NewWalletOrderCancelAllRequest(walletType WalletType) *Wa return &WalletOrderCancelAllRequest{client: s.Client, walletType: walletType} } +func (s *OrderService) NewWalletGetTradesRequest(walletType WalletType) *WalletGetTradesRequest { + return &WalletGetTradesRequest{client: s.Client, walletType: walletType} +} + func (s *OrderService) NewOrderCancelRequest() *OrderCancelRequest { return &OrderCancelRequest{client: s.Client} } @@ -88,6 +94,21 @@ type WalletOrderCancelAllRequest struct { groupID *uint32 `param:"groupID"` } +type Trade = maxapi.Trade + +//go:generate GetRequest -url "/api/v3/wallet/:walletType/trades" -type WalletGetTradesRequest -responseType []Trade +type WalletGetTradesRequest struct { + client requestgen.AuthenticatedAPIClient + + walletType WalletType `param:"walletType,slug,required"` + + market string `param:"market,required"` + from *uint64 `param:"from_id"` + startTime *time.Time `param:"start_time,milliseconds"` + endTime *time.Time `param:"end_time,milliseconds"` + limit *uint64 `param:"limit"` +} + //go:generate PostRequest -url "/api/v3/order" -type OrderCancelRequest -responseType .Order type OrderCancelRequest struct { client requestgen.AuthenticatedAPIClient diff --git a/pkg/exchange/max/maxapi/v3/wallet_get_trades_request_requestgen.go b/pkg/exchange/max/maxapi/v3/wallet_get_trades_request_requestgen.go new file mode 100644 index 000000000..497553185 --- /dev/null +++ b/pkg/exchange/max/maxapi/v3/wallet_get_trades_request_requestgen.go @@ -0,0 +1,233 @@ +// Code generated by "requestgen -method GET -url /api/v3/wallet/:walletType/trades -type WalletGetTradesRequest -responseType []Trade"; DO NOT EDIT. + +package v3 + +import ( + "context" + "encoding/json" + "fmt" + "github.com/c9s/bbgo/pkg/exchange/max/maxapi" + "net/url" + "reflect" + "regexp" + "strconv" + "time" +) + +func (w *WalletGetTradesRequest) Market(market string) *WalletGetTradesRequest { + w.market = market + return w +} + +func (w *WalletGetTradesRequest) From(from uint64) *WalletGetTradesRequest { + w.from = &from + return w +} + +func (w *WalletGetTradesRequest) StartTime(startTime time.Time) *WalletGetTradesRequest { + w.startTime = &startTime + return w +} + +func (w *WalletGetTradesRequest) EndTime(endTime time.Time) *WalletGetTradesRequest { + w.endTime = &endTime + return w +} + +func (w *WalletGetTradesRequest) Limit(limit uint64) *WalletGetTradesRequest { + w.limit = &limit + return w +} + +func (w *WalletGetTradesRequest) WalletType(walletType max.WalletType) *WalletGetTradesRequest { + w.walletType = walletType + return w +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (w *WalletGetTradesRequest) 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 (w *WalletGetTradesRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check market field -> json key market + market := w.market + + // TEMPLATE check-required + if len(market) == 0 { + return nil, fmt.Errorf("market is required, empty string given") + } + // END TEMPLATE check-required + + // assign parameter of market + params["market"] = market + // check from field -> json key from_id + if w.from != nil { + from := *w.from + + // assign parameter of from + params["from_id"] = from + } else { + } + // check startTime field -> json key start_time + if w.startTime != nil { + startTime := *w.startTime + + // assign parameter of startTime + // convert time.Time to milliseconds time stamp + params["start_time"] = strconv.FormatInt(startTime.UnixNano()/int64(time.Millisecond), 10) + } else { + } + // check endTime field -> json key end_time + if w.endTime != nil { + endTime := *w.endTime + + // assign parameter of endTime + // convert time.Time to milliseconds time stamp + params["end_time"] = strconv.FormatInt(endTime.UnixNano()/int64(time.Millisecond), 10) + } else { + } + // check limit field -> json key limit + if w.limit != nil { + limit := *w.limit + + // assign parameter of limit + params["limit"] = limit + } else { + } + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (w *WalletGetTradesRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := w.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if w.isVarSlice(_v) { + w.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 (w *WalletGetTradesRequest) GetParametersJSON() ([]byte, error) { + params, err := w.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 (w *WalletGetTradesRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check walletType field -> json key walletType + walletType := w.walletType + + // TEMPLATE check-required + if len(walletType) == 0 { + return nil, fmt.Errorf("walletType is required, empty string given") + } + // END TEMPLATE check-required + + // assign parameter of walletType + params["walletType"] = walletType + + return params, nil +} + +func (w *WalletGetTradesRequest) 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 (w *WalletGetTradesRequest) 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 (w *WalletGetTradesRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (w *WalletGetTradesRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := w.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (w *WalletGetTradesRequest) Do(ctx context.Context) ([]max.Trade, error) { + + // empty params for GET operation + var params interface{} + query, err := w.GetParametersQuery() + if err != nil { + return nil, err + } + + apiURL := "/api/v3/wallet/:walletType/trades" + slugs, err := w.GetSlugsMap() + if err != nil { + return nil, err + } + + apiURL = w.applySlugsToUrl(apiURL, slugs) + + req, err := w.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := w.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse []max.Trade + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + return apiResponse, nil +}