mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-10 09:11:55 +00:00
max: add restful api endpoint
This commit is contained in:
parent
73d81a4e98
commit
59f27cfe2e
110
bbgo/exchange/max/maxapi/account.go
Normal file
110
bbgo/exchange/max/maxapi/account.go
Normal file
|
@ -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
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
|
||||
|
|
201
bbgo/exchange/max/maxapi/order.go
Normal file
201
bbgo/exchange/max/maxapi/order.go
Normal file
|
@ -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
|
||||
}
|
||||
|
142
bbgo/exchange/max/maxapi/public.go
Normal file
142
bbgo/exchange/max/maxapi/public.go
Normal file
|
@ -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")),
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
347
bbgo/exchange/max/maxapi/restapi.go
Normal file
347
bbgo/exchange/max/maxapi/restapi.go
Normal file
|
@ -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)
|
||||
}
|
131
bbgo/exchange/max/maxapi/trade.go
Normal file
131
bbgo/exchange/max/maxapi/trade.go
Normal file
|
@ -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
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in New Issue
Block a user