mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-22 14:55:16 +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
|
package max
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/hmac"
|
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/hex"
|
|
||||||
)
|
|
||||||
|
|
||||||
type AuthMessage struct {
|
type AuthMessage struct {
|
||||||
Action string `json:"action"`
|
Action string `json:"action"`
|
||||||
APIKey string `json:"apiKey"`
|
APIKey string `json:"apiKey"`
|
||||||
|
@ -19,13 +13,3 @@ type AuthEvent struct {
|
||||||
ID string
|
ID string
|
||||||
Timestamp int64
|
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/gorilla/websocket"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var ErrMessageTypeNotSupported = errors.New("message type currently not supported")
|
var ErrMessageTypeNotSupported = errors.New("message type currently not supported")
|
||||||
|
|
||||||
var logger = log.WithField("exchange", "max")
|
|
||||||
|
|
||||||
// Subscription is used for presenting the subscription metadata.
|
// Subscription is used for presenting the subscription metadata.
|
||||||
// This is used for sending subscribe and unsubscribe requests
|
// This is used for sending subscribe and unsubscribe requests
|
||||||
type Subscription struct {
|
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
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
type Balance struct {
|
type BalanceMessage struct {
|
||||||
Currency string `json:"cu"`
|
Currency string `json:"cu"`
|
||||||
Available string `json:"av"`
|
Available string `json:"av"`
|
||||||
Locked string `json:"l"`
|
Locked string `json:"l"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseBalance(v *fastjson.Value) Balance {
|
func parseBalance(v *fastjson.Value) BalanceMessage {
|
||||||
return Balance{
|
return BalanceMessage{
|
||||||
Currency: string(v.GetStringBytes("cu")),
|
Currency: string(v.GetStringBytes("cu")),
|
||||||
Available: string(v.GetStringBytes("av")),
|
Available: string(v.GetStringBytes("av")),
|
||||||
Locked: string(v.GetStringBytes("l")),
|
Locked: string(v.GetStringBytes("l")),
|
||||||
|
@ -172,7 +172,7 @@ func parseBalance(v *fastjson.Value) Balance {
|
||||||
|
|
||||||
type AccountUpdateEvent struct {
|
type AccountUpdateEvent struct {
|
||||||
BaseEvent
|
BaseEvent
|
||||||
Balances []Balance `json:"B"`
|
Balances []BalanceMessage `json:"B"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func parserAccountUpdateEvent(v *fastjson.Value) (e AccountUpdateEvent) {
|
func parserAccountUpdateEvent(v *fastjson.Value) (e AccountUpdateEvent) {
|
||||||
|
@ -188,7 +188,7 @@ func parserAccountUpdateEvent(v *fastjson.Value) (e AccountUpdateEvent) {
|
||||||
|
|
||||||
type AccountSnapshotEvent struct {
|
type AccountSnapshotEvent struct {
|
||||||
BaseEvent
|
BaseEvent
|
||||||
Balances []Balance `json:"B"`
|
Balances []BalanceMessage `json:"B"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func parserAccountSnapshotEvent(v *fastjson.Value) (e AccountSnapshotEvent) {
|
func parserAccountSnapshotEvent(v *fastjson.Value) (e AccountSnapshotEvent) {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user