diff --git a/bbgo/exchange/max/maxapi/account.go b/bbgo/exchange/max/maxapi/account.go new file mode 100644 index 000000000..8f971fa7c --- /dev/null +++ b/bbgo/exchange/max/maxapi/account.go @@ -0,0 +1,110 @@ +package max + +type AccountService struct { + client *RestClient +} + +// Account is for max rest api v2, Balance and Type will be conflict with types.PrivateBalanceUpdate +type Account struct { + Currency string `json:"currency"` + Balance string `json:"balance"` + Locked string `json:"locked"` + Type string `json:"type"` +} + +// Balance is for kingfisher +type Balance struct { + Currency string + Available int64 + Locked int64 + Total int64 +} + +type UserBank struct { + Branch string `json:"branch"` + Name string `json:"name"` + Account string `json:"account"` + State string `json:"state"` +} + +type UserInfo struct { + Sn string `json:"sn"` + Name string `json:"name"` + Type string `json:"member_type"` + Level int `json:"level"` + Email string `json:"email"` + Accounts []Account `json:"accounts"` + Bank *UserBank `json:"bank,omitempty"` + IsFrozen bool `json:"is_frozen"` + IsActivated bool `json:"is_activated"` + KycApproved bool `json:"kyc_approved"` + KycState string `json:"kyc_state"` + PhoneSet bool `json:"phone_set"` + PhoneNumber string `json:"phone_number"` + ProfileVerified bool `json:"profile_verified"` + CountryCode string `json:"country_code"` + IdentityNumber string `json:"identity_number"` + WithDrawable bool `json:"withdrawable"` + ReferralCode string `json:"referral_code"` +} + +func (s *AccountService) Account(currency string) (*Account, error) { + req, err := s.client.newAuthenticatedRequest("GET", "v2/members/accounts/"+currency, nil) + if err != nil { + return nil, err + } + + response, err := s.client.sendRequest(req) + if err != nil { + return nil, err + } + + var account Account + err = response.DecodeJSON(&account) + if err != nil { + return nil, err + } + + return &account, nil +} + +func (s *AccountService) Accounts() ([]Account, error) { + req, err := s.client.newAuthenticatedRequest("GET", "v2/members/accounts", nil) + if err != nil { + return nil, err + } + + response, err := s.client.sendRequest(req) + if err != nil { + return nil, err + } + + var accounts []Account + err = response.DecodeJSON(&accounts) + if err != nil { + return nil, err + } + + return accounts, nil +} + +// Me returns the current user info by the current used MAX key and secret +func (s *AccountService) Me() (*UserInfo, error) { + req, err := s.client.newAuthenticatedRequest("GET", "v2/members/me", nil) + if err != nil { + return nil, err + } + + response, err := s.client.sendRequest(req) + if err != nil { + return nil, err + } + + var m = UserInfo{} + err = response.DecodeJSON(&m) + if err != nil { + return nil, err + } + + return &m, nil +} diff --git a/bbgo/exchange/max/maxapi/auth.go b/bbgo/exchange/max/maxapi/auth.go index 492cf3b3e..c353c56dd 100644 --- a/bbgo/exchange/max/maxapi/auth.go +++ b/bbgo/exchange/max/maxapi/auth.go @@ -1,11 +1,5 @@ package max -import ( - "crypto/hmac" - "crypto/sha256" - "encoding/hex" -) - type AuthMessage struct { Action string `json:"action"` APIKey string `json:"apiKey"` @@ -19,13 +13,3 @@ type AuthEvent struct { ID string Timestamp int64 } - -func signPayload(payload string, secret string) string { - var sig = hmac.New(sha256.New, []byte(secret)) - _, err := sig.Write([]byte(payload)) - if err != nil { - return "" - } - return hex.EncodeToString(sig.Sum(nil)) -} - diff --git a/bbgo/exchange/max/maxapi/order.go b/bbgo/exchange/max/maxapi/order.go new file mode 100644 index 000000000..cd53c025a --- /dev/null +++ b/bbgo/exchange/max/maxapi/order.go @@ -0,0 +1,201 @@ +package max + +import ( + "strconv" + "time" + + "github.com/pkg/errors" +) + +type OrderStateToQuery int + +const ( + All = iota + Active + Closed +) + +type OrderType string + +// Order types that the API can return. +const ( + OrderTypeMarket = OrderType("market") + OrderTypeLimit = OrderType("limit") +) + +// OrderService manages the Order endpoint. +type OrderService struct { + client *RestClient +} + +// Order represents one returned order (POST order/GET order/GET orders) on the max platform. +type Order struct { + ID uint64 `json:"id,omitempty" db:"exchange_id"` + Side string `json:"side" db:"side"` + OrderType string `json:"ord_type" db:"order_type"` + Price string `json:"price" db:"price"` + AveragePrice string `json:"avg_price,omitempty" db:"average_price"` + State string `json:"state,omitempty" db:"state"` + Market string `json:"market" db:"market"` + Volume string `json:"volume" db:"volume"` + RemainingVolume string `json:"remaining_volume,omitempty" db:"remaining_volume"` + ExecutedVolume string `json:"executed_volume,omitempty" db:"executed_volume"` + TradesCount int64 `json:"trades_count,omitempty" db:"trades_count"` + GroupID int64 `json:"group_id,omitempty" db:"group_id"` + ClientOID string `json:"client_oid,omitempty" db:"client_oid"` + CreatedAt time.Time `db:"created_at"` + CreatedAtMs int64 `json:"created_at_in_ms,omitempty"` + InsertedAt time.Time `db:"inserted_at"` +} + +// All returns all orders for the authenticated account. +func (s *OrderService) All(market string, limit, page int, state OrderStateToQuery) ([]Order, error) { + var states []string + switch state { + case All: + states = []string{"done", "cancel", "wait", "convert"} + case Active: + states = []string{"wait", "convert"} + case Closed: + states = []string{"done", "cancel"} + default: + states = []string{"wait", "convert"} + } + + payload := map[string]interface{}{ + "market": market, + "limit": limit, + "page": page, + "state": states, + "order_by": "desc", + } + req, err := s.client.newAuthenticatedRequest("GET", "v2/orders", payload) + if err != nil { + return nil, err + } + + response, err := s.client.sendRequest(req) + if err != nil { + return nil, err + } + + var orders []Order + if err := response.DecodeJSON(&orders); err != nil { + return nil, err + } + + return orders, nil +} + +// CancelAll active orders for the authenticated account. +func (s *OrderService) CancelAll(side string, market string) error { + payload := map[string]interface{}{} + if side == "buy" || side == "sell" { + payload["side"] = side + } + if market != "all" { + payload["market"] = market + } + + req, err := s.client.newAuthenticatedRequest("POST", "v2/orders/clear", payload) + if err != nil { + return err + } + + _, err = s.client.sendRequest(req) + if err != nil { + return err + } + + return nil +} + +// Options carry the option fields for REST API +type Options map[string]interface{} + +// Create a new order. +func (s *OrderService) Create(market string, side string, volume float64, price float64, orderType string, options Options) (*Order, error) { + options["market"] = market + options["volume"] = strconv.FormatFloat(volume, 'f', -1, 64) + options["price"] = strconv.FormatFloat(price, 'f', -1, 64) + options["side"] = side + options["ord_type"] = orderType + response, err := s.client.sendAuthenticatedRequest("POST", "v2/orders", options) + if err != nil { + return nil, err + } + + var order = Order{} + if err := response.DecodeJSON(&order); err != nil { + return nil, err + } + + return &order, nil +} + +// Cancel the order with id `orderID`. +func (s *OrderService) Cancel(orderID uint64) error { + payload := map[string]interface{}{ + "id": orderID, + } + + req, err := s.client.newAuthenticatedRequest("POST", "v2/order/delete", payload) + if err != nil { + return err + } + + _, err = s.client.sendRequest(req) + if err != nil { + return err + } + + return nil +} + +// Status retrieves the given order from the API. +func (s *OrderService) Get(orderID uint64) (*Order, error) { + + payload := map[string]interface{}{ + "id": orderID, + } + + req, err := s.client.newAuthenticatedRequest("GET", "v2/order", payload) + + if err != nil { + return &Order{}, err + } + + response, err := s.client.sendRequest(req) + if err != nil { + return nil, err + } + + var order = Order{} + + if err := response.DecodeJSON(&order); err != nil { + return nil, err + } + + return &order, nil +} + +// Create multiple order in a single request +func (s *OrderService) CreateMulti(market string, orders []Order) ([]Order, error) { + var returnOrders []Order + req, err := s.client.newAuthenticatedRequest("POST", "v2/orders/multi", map[string]interface{}{ + "market": market, + "orders": orders, + }) + if err != nil { + return returnOrders, errors.Wrapf(err, "failed to create %s orders", market) + } + response, err := s.client.sendRequest(req) + if err != nil { + return returnOrders, err + } + if errJson := response.DecodeJSON(&returnOrders); errJson != nil { + return returnOrders, errJson + } + return returnOrders, err +} + diff --git a/bbgo/exchange/max/maxapi/public.go b/bbgo/exchange/max/maxapi/public.go new file mode 100644 index 000000000..8c8c10a99 --- /dev/null +++ b/bbgo/exchange/max/maxapi/public.go @@ -0,0 +1,142 @@ +package max + +import ( + "net/url" + "time" + + "github.com/valyala/fastjson" +) + +type PublicService struct { + client *RestClient +} + +type Market struct { + ID string `json:"id"` + Name string `json:"name"` + BaseUnit string `json:"base_unit"` + BaseUnitPrecision int `json:"base_unit_precision"` + QuoteUnit string `json:"quote_unit"` + QuoteUnitPrecision int `json:"quote_unit_precision"` +} + +type Ticker struct { + Time time.Time + + At int64 `json:"at"` + Buy string `json:"buy"` + Sell string `json:"sell"` + Open string `json:"open"` + High string `json:"high"` + Low string `json:"low"` + Last string `json:"last"` + Volume string `json:"vol"` + VolumeInBTC string `json:"vol_in_btc"` +} + +func (s *PublicService) Timestamp() (serverTimestamp int64, err error) { + // sync timestamp with server + req, err := s.client.newRequest("GET", "v2/timestamp", nil, nil) + if err != nil { + return 0, err + } + + response, err := s.client.sendRequest(req) + if err != nil { + return 0, err + } + + err = response.DecodeJSON(&serverTimestamp) + if err != nil { + return 0, err + } + + return serverTimestamp, nil +} + +func (s *PublicService) Markets() ([]Market, error) { + req, err := s.client.newRequest("GET", "v2/markets", url.Values{}, nil) + if err != nil { + return nil, err + } + + response, err := s.client.sendRequest(req) + if err != nil { + return nil, err + } + + var m []Market + if err := response.DecodeJSON(&m); err != nil { + return nil, err + } + + return m, nil +} + +func (s *PublicService) Tickers() (map[string]Ticker, error) { + var endPoint = "v2/tickers" + req, err := s.client.newRequest("GET", endPoint, url.Values{}, nil) + if err != nil { + return nil, err + } + + response, err := s.client.sendRequest(req) + if err != nil { + return nil, err + } + + v, err := fastjson.ParseBytes(response.Body) + if err != nil { + return nil, err + } + + o, err := v.Object() + if err != nil { + return nil, err + } + + var tickers = make(map[string]Ticker) + o.Visit(func(key []byte, v *fastjson.Value) { + var ticker = mustParseTicker(v) + tickers[string(key)] = ticker + }) + + return tickers, nil +} + +func (s *PublicService) Ticker(market string) (*Ticker, error) { + var endPoint = "v2/tickers/" + market + req, err := s.client.newRequest("GET", endPoint, url.Values{}, nil) + if err != nil { + return nil, err + } + + response, err := s.client.sendRequest(req) + if err != nil { + return nil, err + } + + v, err := fastjson.ParseBytes(response.Body) + if err != nil { + return nil, err + } + + var ticker = mustParseTicker(v) + return &ticker, nil +} + +func mustParseTicker(v *fastjson.Value) Ticker { + var at = v.GetInt64("at") + return Ticker{ + Time: time.Unix(at, 0), + At: at, + Buy: string(v.GetStringBytes("buy")), + Sell: string(v.GetStringBytes("sell")), + Volume: string(v.GetStringBytes("vol")), + VolumeInBTC: string(v.GetStringBytes("vol_in_btc")), + Last: string(v.GetStringBytes("last")), + Open: string(v.GetStringBytes("open")), + High: string(v.GetStringBytes("high")), + Low: string(v.GetStringBytes("low")), + } +} diff --git a/bbgo/exchange/max/maxapi/public_websocket.go b/bbgo/exchange/max/maxapi/public_websocket.go index 5fdb9faf0..3c9f98fea 100644 --- a/bbgo/exchange/max/maxapi/public_websocket.go +++ b/bbgo/exchange/max/maxapi/public_websocket.go @@ -9,13 +9,10 @@ import ( "github.com/gorilla/websocket" "github.com/pkg/errors" - log "github.com/sirupsen/logrus" ) var ErrMessageTypeNotSupported = errors.New("message type currently not supported") -var logger = log.WithField("exchange", "max") - // Subscription is used for presenting the subscription metadata. // This is used for sending subscribe and unsubscribe requests type Subscription struct { diff --git a/bbgo/exchange/max/maxapi/restapi.go b/bbgo/exchange/max/maxapi/restapi.go new file mode 100644 index 000000000..4b7764dec --- /dev/null +++ b/bbgo/exchange/max/maxapi/restapi.go @@ -0,0 +1,347 @@ +package max + +import ( + "bytes" + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "encoding/json" + "fmt" + "io/ioutil" + "math" + "net/http" + "net/url" + "regexp" + "strconv" + "sync/atomic" + "time" + + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" +) + +const ( + // ProductionAPIURL is the official MAX API v2 Endpoint + ProductionAPIURL = "https://max-api.maicoin.com/api/v2" + + UserAgent = "bbgo/1.0" + + defaultHTTPTimeout = time.Second * 15 +) + +var logger = log.WithField("exchange", "max") + +var htmlTagPattern = regexp.MustCompile("<[/]?[a-zA-Z-]+.*?>") + +// The following variables are used for nonce. + +// timeOffset is used for nonce +var timeOffset int64 = 0 + +// serverTimestamp is used for storing the server timestamp, default to Now +var serverTimestamp = time.Now().Unix() + +// reqCount is used for nonce, this variable counts the API request count. +var reqCount int64 = 0 + +// Response is wrapper for standard http.Response and provides +// more methods. +type Response struct { + *http.Response + + // Body overrides the composited Body field. + Body []byte +} + +// newResponse is a wrapper of the http.Response instance, it reads the response body and close the file. +func newResponse(r *http.Response) (response *Response, err error) { + body, err := ioutil.ReadAll(r.Body) + if err != nil { + return nil, err + } + + err = r.Body.Close() + response = &Response{Response: r, Body: body} + return response, err +} + +// String converts response body to string. +// An empty string will be returned if error. +func (r *Response) String() string { + return string(r.Body) +} + +func (r *Response) DecodeJSON(o interface{}) error { + return json.Unmarshal(r.Body, o) +} + +type RestClient struct { + client *http.Client + + BaseURL *url.URL + + // Authentication + APIKey string + APISecret string + + AccountService *AccountService + PublicService *PublicService + TradeService *TradeService + OrderService *OrderService + // OrderBookService *OrderBookService + // MaxTokenService *MaxTokenService + // MaxKLineService *KLineService + // CreditService *CreditService +} + +func NewRestClientWithHttpClient(baseURL string, httpClient *http.Client) *RestClient { + u, err := url.Parse(baseURL) + if err != nil { + panic(err) + } + + var client = &RestClient{ + client: httpClient, + BaseURL: u, + } + + client.AccountService = &AccountService{client} + client.TradeService = &TradeService{client} + client.PublicService = &PublicService{client} + client.OrderService = &OrderService{client} + // client.OrderBookService = &OrderBookService{client} + // client.MaxTokenService = &MaxTokenService{client} + // client.MaxKLineService = &KLineService{client} + // client.CreditService = &CreditService{client} + client.initNonce() + return client +} + +func NewRestClient(baseURL string) *RestClient { + return NewRestClientWithHttpClient(baseURL, &http.Client{ + Timeout: defaultHTTPTimeout, + }) +} + +// Auth sets api key and secret for usage is requests that requires authentication. +func (c *RestClient) Auth(key string, secret string) *RestClient { + c.APIKey = key + c.APISecret = secret + return c +} + +func (c *RestClient) initNonce() { + var clientTime = time.Now() + var err error + serverTimestamp, err = c.PublicService.Timestamp() + if err != nil { + logger.WithError(err).Panic("failed to sync timestamp with Max") + } + + // 1 is for the request count mod 0.000 to 0.999 + timeOffset = serverTimestamp - clientTime.Unix() - 1 +} + +func (c *RestClient) getNonce() int64 { + var seconds = time.Now().Unix() + var rc = atomic.AddInt64(&reqCount, 1) + return (seconds+timeOffset)*1000 + int64(math.Mod(float64(rc), 1000.0)) +} + +// NewRequest create new API request. Relative url can be provided in refURL. +func (c *RestClient) newRequest(method string, refURL string, params url.Values, body []byte) (*http.Request, error) { + rel, err := url.Parse(refURL) + if err != nil { + return nil, err + } + if params != nil { + rel.RawQuery = params.Encode() + } + var req *http.Request + u := c.BaseURL.ResolveReference(rel) + + req, err = http.NewRequest(method, u.String(), bytes.NewReader(body)) + if err != nil { + return nil, err + } + + return req, nil +} + +// newAuthenticatedRequest creates new http request for authenticated routes. +func (c *RestClient) newAuthenticatedRequest(m string, refURL string, data map[string]interface{}) (*http.Request, error) { + rel, err := url.Parse(refURL) + if err != nil { + return nil, err + } + + payload := map[string]interface{}{ + "nonce": c.getNonce(), + "path": c.BaseURL.ResolveReference(rel).Path, + } + + for k, v := range data { + payload[k] = v + } + + p, err := json.Marshal(payload) + if err != nil { + return nil, err + } + + req, err := c.newRequest(m, refURL, nil, p) + if err != nil { + return nil, err + } + + encoded := base64.StdEncoding.EncodeToString(p) + + req.Header.Add("Content-Type", "application/json") + req.Header.Add("Accept", "application/json") + req.Header.Add("X-MAX-ACCESSKEY", c.APIKey) + req.Header.Add("X-MAX-PAYLOAD", encoded) + req.Header.Add("X-MAX-SIGNATURE", signPayload(encoded, c.APISecret)) + + return req, nil +} + +func signPayload(payload string, secret string) string { + var sig = hmac.New(sha256.New, []byte(secret)) + _, err := sig.Write([]byte(payload)) + if err != nil { + return "" + } + return hex.EncodeToString(sig.Sum(nil)) +} + +func (c *RestClient) Do(req *http.Request) (resp *http.Response, err error) { + req.Header.Set("User-Agent", UserAgent) + return c.client.Do(req) +} + +// sendRequest sends the request to the API server and handle the response +func (c *RestClient) sendRequest(req *http.Request) (*Response, error) { + resp, err := c.Do(req) + if err != nil { + return nil, err + } + + // newResponse reads the response body and return a new Response object + response, err := newResponse(resp) + if err != nil { + return response, err + } + + // Check error, if there is an error, return the ErrorResponse struct type + if isError(response) { + errorResponse, err := toErrorResponse(response) + if err != nil { + return response, err + } + return response, errorResponse + } + + return response, nil +} + +func (c *RestClient) sendAuthenticatedRequest(m string, refURL string, data map[string]interface{}) (*Response, error) { + req, err := c.newAuthenticatedRequest(m, refURL, data) + if err != nil { + return nil, err + } + response, err := c.sendRequest(req) + if err != nil { + return nil, err + } + return response, err +} + +// FIXME: should deprecate the polling usage from the websocket struct +func (c *RestClient) GetTrades(market string, lastTradeID int64) ([]byte, error) { + params := url.Values{} + params.Add("market", market) + if lastTradeID > 0 { + params.Add("from", strconv.Itoa(int(lastTradeID))) + } + + return c.get("/trades", params) +} + +// get sends GET http request to the api endpoint, the urlPath must start with a slash '/' +func (c *RestClient) get(urlPath string, values url.Values) ([]byte, error) { + var reqURL = c.BaseURL.String() + urlPath + + // Create request + req, err := http.NewRequest("GET", reqURL, nil) + if err != nil { + return nil, fmt.Errorf("could not init request: %s", err.Error()) + } + + req.URL.RawQuery = values.Encode() + req.Header.Add("User-Agent", UserAgent) + + // Execute request + resp, err := c.client.Do(req) + if err != nil { + return nil, fmt.Errorf("could not execute request: %s", err.Error()) + } + defer resp.Body.Close() + + // Load request + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("could not read response: %s", err.Error()) + } + + return body, nil +} + +// ErrorResponse is the custom error type that is returned if the API returns an +// error. +type ErrorField struct { + Code int `json:"code"` + Message string `json:"message"` +} + +type ErrorResponse struct { + *Response + Err ErrorField `json:"error"` +} + +func (r *ErrorResponse) Error() string { + return fmt.Sprintf("%s %s: %d %d %s", + r.Response.Response.Request.Method, + r.Response.Response.Request.URL.String(), + r.Response.Response.StatusCode, + r.Err.Code, + r.Err.Message, + ) +} + +// isError check the response status code so see if a response is an error. +func isError(response *Response) bool { + var c = response.StatusCode + return c < 200 || c > 299 +} + +// toErrorResponse tries to convert/parse the server response to the standard Error interface object +func toErrorResponse(response *Response) (errorResponse *ErrorResponse, err error) { + errorResponse = &ErrorResponse{Response: response} + + contentType := response.Header.Get("content-type") + switch contentType { + case "text/json", "application/json", "application/json; charset=utf-8": + var err = response.DecodeJSON(errorResponse) + if err != nil { + return errorResponse, errors.Wrapf(err, "failed to decode json for response: %d %s", response.StatusCode, string(response.Body)) + } + return errorResponse, nil + case "text/html": + // convert 5xx error from the HTML page to the ErrorResponse + errorResponse.Err.Message = htmlTagPattern.ReplaceAllLiteralString(string(response.Body), "") + return errorResponse, nil + } + + return errorResponse, fmt.Errorf("unexpected response content type %s", contentType) +} diff --git a/bbgo/exchange/max/maxapi/trade.go b/bbgo/exchange/max/maxapi/trade.go new file mode 100644 index 000000000..b407fc4e5 --- /dev/null +++ b/bbgo/exchange/max/maxapi/trade.go @@ -0,0 +1,131 @@ +package max + +import ( + "net/url" + "strconv" + "time" +) + +// 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 int64 `json:"created_at_in_ms"` + Side string `json:"side" db:"side"` + OrderID uint64 `json:"order_id" db:"order_id"` + Fee string `json:"fee" db:"fee"` // float number as string + FeeCurrency string `json:"fee_currency" db:"fee_currency"` + CreatedAtInDB time.Time `db:"created_at"` + InsertedAt time.Time `db:"inserted_at"` +} + +type QueryTradeOptions struct { + Market string `json:"market"` + Timestamp int64 `json:"timestamp,omitempty"` + From int64 `json:"from,omitempty"` + To int64 `json:"to,omitempty"` + OrderBy string `json:"order_by,omitempty"` + Page int `json:"page,omitempty"` + Offset int `json:"offset,omitempty"` + Limit int64 `json:"limit,omitempty"` +} + +type TradeService struct { + client *RestClient +} + +func (options *QueryTradeOptions) Map() map[string]interface{} { + var data = map[string]interface{}{} + data["market"] = options.Market + + if options.Limit > 0 { + data["limit"] = options.Limit + } + + if options.Timestamp > 0 { + data["timestamp"] = options.Timestamp + } + + if options.From >= 0 { + data["from"] = options.From + } + + if options.To > options.From { + data["to"] = options.To + } + if len(options.OrderBy) > 0 { + // could be "asc" or "desc" + data["order_by"] = options.OrderBy + } + + return data +} + +func (options *QueryTradeOptions) Params() url.Values { + var params = url.Values{} + params.Add("market", options.Market) + + if options.Limit > 0 { + params.Add("limit", strconv.FormatInt(options.Limit, 10)) + } + if options.Timestamp > 0 { + params.Add("timestamp", strconv.FormatInt(options.Timestamp, 10)) + } + if options.From >= 0 { + params.Add("from", strconv.FormatInt(options.From, 10)) + } + if options.To > options.From { + params.Add("to", strconv.FormatInt(options.To, 10)) + } + if len(options.OrderBy) > 0 { + // could be "asc" or "desc" + params.Add("order_by", options.OrderBy) + } + return params +} + +func (s *TradeService) MyTrades(options QueryTradeOptions) ([]Trade, error) { + req, err := s.client.newAuthenticatedRequest("GET", "v2/trades/my", options.Map()) + if err != nil { + return nil, err + } + + response, err := s.client.sendRequest(req) + if err != nil { + return nil, err + } + + var v []Trade + if err := response.DecodeJSON(&v); err != nil { + return nil, err + } + + return v, nil +} + +func (s *TradeService) Trades(options QueryTradeOptions) ([]Trade, error) { + var params = options.Params() + + req, err := s.client.newRequest("GET", "v2/trades", params, nil) + if err != nil { + return nil, err + } + + response, err := s.client.sendRequest(req) + if err != nil { + return nil, err + } + + var v []Trade + if err := response.DecodeJSON(&v); err != nil { + return nil, err + } + + return v, nil +} + diff --git a/bbgo/exchange/max/maxapi/userdata.go b/bbgo/exchange/max/maxapi/userdata.go index 478663a89..d4fb623d4 100644 --- a/bbgo/exchange/max/maxapi/userdata.go +++ b/bbgo/exchange/max/maxapi/userdata.go @@ -155,14 +155,14 @@ func parseTradeSnapshotEvent(v *fastjson.Value) (e TradeSnapshotEvent) { return e } -type Balance struct { +type BalanceMessage struct { Currency string `json:"cu"` Available string `json:"av"` Locked string `json:"l"` } -func parseBalance(v *fastjson.Value) Balance { - return Balance{ +func parseBalance(v *fastjson.Value) BalanceMessage { + return BalanceMessage{ Currency: string(v.GetStringBytes("cu")), Available: string(v.GetStringBytes("av")), Locked: string(v.GetStringBytes("l")), @@ -172,7 +172,7 @@ func parseBalance(v *fastjson.Value) Balance { type AccountUpdateEvent struct { BaseEvent - Balances []Balance `json:"B"` + Balances []BalanceMessage `json:"B"` } func parserAccountUpdateEvent(v *fastjson.Value) (e AccountUpdateEvent) { @@ -188,7 +188,7 @@ func parserAccountUpdateEvent(v *fastjson.Value) (e AccountUpdateEvent) { type AccountSnapshotEvent struct { BaseEvent - Balances []Balance `json:"B"` + Balances []BalanceMessage `json:"B"` } func parserAccountSnapshotEvent(v *fastjson.Value) (e AccountSnapshotEvent) {