Merge pull request #636 from c9s/feature/max-margin-wallet

fix: max: fix trades/orders parsing
This commit is contained in:
Yo-An Lin 2022-05-27 19:55:22 +08:00 committed by GitHub
commit 5c5a88fe0e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 374 additions and 655 deletions

View File

@ -4,6 +4,28 @@ sessions:
exchange: max exchange: max
margin: true margin: true
sync:
# userDataStream is used to sync the trading data in real-time
# it uses the websocket connection to insert the trades
userDataStream:
trades: false
filledOrders: false
# since is the start date of your trading data
since: 2019-11-01
# sessions is the list of session names you want to sync
# by default, BBGO sync all your available sessions.
sessions:
- max_margin
# symbols is the list of symbols you want to sync
# by default, BBGO try to guess your symbols by your existing account balances.
symbols:
- BTCUSDT
- ETHUSDT
exchangeStrategies: exchangeStrategies:
- on: max_margin - on: max_margin

View File

@ -168,6 +168,7 @@ func toGlobalOrders(maxOrders []max.Order) (orders []types.Order, err error) {
func toGlobalOrder(maxOrder max.Order) (*types.Order, error) { func toGlobalOrder(maxOrder max.Order) (*types.Order, error) {
executedVolume := maxOrder.ExecutedVolume executedVolume := maxOrder.ExecutedVolume
remainingVolume := maxOrder.RemainingVolume remainingVolume := maxOrder.RemainingVolume
isMargin := maxOrder.WalletType == max.WalletTypeMargin
return &types.Order{ return &types.Order{
SubmitOrder: types.SubmitOrder{ SubmitOrder: types.SubmitOrder{
@ -185,52 +186,33 @@ func toGlobalOrder(maxOrder max.Order) (*types.Order, error) {
OrderID: maxOrder.ID, OrderID: maxOrder.ID,
Status: toGlobalOrderStatus(maxOrder.State, executedVolume, remainingVolume), Status: toGlobalOrderStatus(maxOrder.State, executedVolume, remainingVolume),
ExecutedQuantity: executedVolume, ExecutedQuantity: executedVolume,
CreationTime: types.Time(maxOrder.CreatedAtMs.Time()), CreationTime: types.Time(maxOrder.CreatedAt.Time()),
UpdateTime: types.Time(maxOrder.CreatedAtMs.Time()), UpdateTime: types.Time(maxOrder.CreatedAt.Time()),
IsMargin: isMargin,
IsIsolated: false, // isolated margin is not supported
}, nil }, nil
} }
func toGlobalTrade(t max.Trade) (*types.Trade, error) { func toGlobalTrade(t max.Trade) (*types.Trade, error) {
// skip trade ID that is the same. however this should not happen isMargin := t.WalletType == max.WalletTypeMargin
var side = toGlobalSideType(t.Side) side := toGlobalSideType(t.Side)
// trade time
mts := t.CreatedAtMilliSeconds
price, err := fixedpoint.NewFromString(t.Price)
if err != nil {
return nil, err
}
quantity, err := fixedpoint.NewFromString(t.Volume)
if err != nil {
return nil, err
}
quoteQuantity, err := fixedpoint.NewFromString(t.Funds)
if err != nil {
return nil, err
}
fee, err := fixedpoint.NewFromString(t.Fee)
if err != nil {
return nil, err
}
return &types.Trade{ return &types.Trade{
ID: t.ID, ID: t.ID,
OrderID: t.OrderID, OrderID: t.OrderID,
Price: price, Price: t.Price,
Symbol: toGlobalSymbol(t.Market), Symbol: toGlobalSymbol(t.Market),
Exchange: "max", Exchange: types.ExchangeMax,
Quantity: quantity, Quantity: t.Volume,
Side: side, Side: side,
IsBuyer: t.IsBuyer(), IsBuyer: t.IsBuyer(),
IsMaker: t.IsMaker(), IsMaker: t.IsMaker(),
Fee: fee, Fee: t.Fee,
FeeCurrency: toGlobalCurrency(t.FeeCurrency), FeeCurrency: toGlobalCurrency(t.FeeCurrency),
QuoteQuantity: quoteQuantity, QuoteQuantity: t.Funds,
Time: types.Time(mts), Time: types.Time(t.CreatedAt),
IsMargin: isMargin,
IsIsolated: false,
IsFutures: false,
}, nil }, nil
} }
@ -297,16 +279,6 @@ func convertWebSocketTrade(t max.TradeUpdate) (*types.Trade, error) {
} }
func convertWebSocketOrderUpdate(u max.OrderUpdate) (*types.Order, error) { func convertWebSocketOrderUpdate(u max.OrderUpdate) (*types.Order, error) {
executedVolume, err := fixedpoint.NewFromString(u.ExecutedVolume)
if err != nil {
return nil, err
}
remainingVolume, err := fixedpoint.NewFromString(u.RemainingVolume)
if err != nil {
return nil, err
}
timeInForce := types.TimeInForceGTC timeInForce := types.TimeInForceGTC
if u.OrderType == max.OrderTypeIOCLimit { if u.OrderType == max.OrderTypeIOCLimit {
timeInForce = types.TimeInForceIOC timeInForce = types.TimeInForceIOC
@ -318,16 +290,16 @@ func convertWebSocketOrderUpdate(u max.OrderUpdate) (*types.Order, error) {
Symbol: toGlobalSymbol(u.Market), Symbol: toGlobalSymbol(u.Market),
Side: toGlobalSideType(u.Side), Side: toGlobalSideType(u.Side),
Type: toGlobalOrderType(u.OrderType), Type: toGlobalOrderType(u.OrderType),
Quantity: fixedpoint.MustNewFromString(u.Volume), Quantity: u.Volume,
Price: fixedpoint.MustNewFromString(u.Price), Price: u.Price,
StopPrice: fixedpoint.MustNewFromString(u.StopPrice), StopPrice: u.StopPrice,
TimeInForce: timeInForce, // MAX only supports GTC TimeInForce: timeInForce, // MAX only supports GTC
GroupID: u.GroupID, GroupID: u.GroupID,
}, },
Exchange: types.ExchangeMax, Exchange: types.ExchangeMax,
OrderID: u.ID, OrderID: u.ID,
Status: toGlobalOrderStatus(u.State, executedVolume, remainingVolume), Status: toGlobalOrderStatus(u.State, u.ExecutedVolume, u.RemainingVolume),
ExecutedQuantity: executedVolume, ExecutedQuantity: u.ExecutedVolume,
CreationTime: types.Time(time.Unix(0, u.CreatedAtMs*int64(time.Millisecond))), CreationTime: types.Time(time.Unix(0, u.CreatedAtMs*int64(time.Millisecond))),
UpdateTime: types.Time(time.Unix(0, u.CreatedAtMs*int64(time.Millisecond))), UpdateTime: types.Time(time.Unix(0, u.CreatedAtMs*int64(time.Millisecond))),
}, nil }, nil

View File

@ -35,6 +35,7 @@ type Exchange struct {
client *maxapi.RestClient client *maxapi.RestClient
v3order *v3.OrderService v3order *v3.OrderService
v3margin *v3.MarginService
} }
func New(key, secret string) *Exchange { func New(key, secret string) *Exchange {
@ -50,6 +51,7 @@ func New(key, secret string) *Exchange {
key: key, key: key,
secret: secret, secret: secret,
v3order: &v3.OrderService{Client: client}, v3order: &v3.OrderService{Client: client},
v3margin: &v3.MarginService{Client: client},
} }
} }
@ -573,11 +575,27 @@ func (e *Exchange) QueryAccount(ctx context.Context) (*types.Account, error) {
// MAX returns the fee rate in the following format: // MAX returns the fee rate in the following format:
// "maker_fee": 0.0005 -> 0.05% // "maker_fee": 0.0005 -> 0.05%
// "taker_fee": 0.0015 -> 0.15% // "taker_fee": 0.0015 -> 0.15%
a := &types.Account{ a := &types.Account{
AccountType: types.AccountTypeSpot,
MarginLevel: fixedpoint.Zero,
MakerFeeRate: fixedpoint.NewFromFloat(vipLevel.Current.MakerFee), // 0.15% = 0.0015 MakerFeeRate: fixedpoint.NewFromFloat(vipLevel.Current.MakerFee), // 0.15% = 0.0015
TakerFeeRate: fixedpoint.NewFromFloat(vipLevel.Current.TakerFee), // 0.15% = 0.0015 TakerFeeRate: fixedpoint.NewFromFloat(vipLevel.Current.TakerFee), // 0.15% = 0.0015
} }
if e.MarginSettings.IsMargin {
a.AccountType = types.AccountTypeMargin
req := e.v3margin.NewGetMarginADRatioRequest()
adRatio, err := req.Do(ctx)
if err != nil {
return a, err
}
a.MarginLevel = adRatio.AdRatio
a.TotalAccountValue = adRatio.AssetInUsdt
}
a.UpdateBalances(balances) a.UpdateBalances(balances)
return a, nil return a, nil
} }
@ -773,11 +791,17 @@ func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *type
return nil, err return nil, err
} }
req := e.client.TradeService.NewGetPrivateTradeRequest() market := toLocalSymbol(symbol)
req.Market(toLocalSymbol(symbol)) walletType := maxapi.WalletTypeSpot
if e.MarginSettings.IsMargin {
walletType = maxapi.WalletTypeMargin
}
req := e.v3order.NewWalletGetTradesRequest(walletType)
req.Market(market)
if options.Limit > 0 { if options.Limit > 0 {
req.Limit(options.Limit) req.Limit(uint64(options.Limit))
} else { } else {
req.Limit(1000) req.Limit(1000)
} }
@ -785,12 +809,9 @@ func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *type
// MAX uses exclusive last trade ID // MAX uses exclusive last trade ID
// the timestamp parameter is used for reverse order, we can't use it. // the timestamp parameter is used for reverse order, we can't use it.
if options.LastTradeID > 0 { if options.LastTradeID > 0 {
req.From(int64(options.LastTradeID)) req.From(options.LastTradeID)
} }
// make it compatible with binance, we need the last trade id for the next page.
req.OrderBy("asc")
maxTrades, err := req.Do(ctx) maxTrades, err := req.Do(ctx)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -1,174 +0,0 @@
// Code generated by "requestgen -method GET -url v2/orders/history -type GetOrderHistoryRequest -responseType []Order"; DO NOT EDIT.
package max
import (
"context"
"encoding/json"
"fmt"
"net/url"
"reflect"
"regexp"
)
func (g *GetOrderHistoryRequest) Market(market string) *GetOrderHistoryRequest {
g.market = market
return g
}
func (g *GetOrderHistoryRequest) FromID(fromID uint64) *GetOrderHistoryRequest {
g.fromID = &fromID
return g
}
func (g *GetOrderHistoryRequest) Limit(limit uint) *GetOrderHistoryRequest {
g.limit = &limit
return g
}
// GetQueryParameters builds and checks the query parameters and returns url.Values
func (g *GetOrderHistoryRequest) GetQueryParameters() (url.Values, error) {
var params = map[string]interface{}{}
query := url.Values{}
for _k, _v := range params {
query.Add(_k, fmt.Sprintf("%v", _v))
}
return query, nil
}
// GetParameters builds and checks the parameters and return the result in a map object
func (g *GetOrderHistoryRequest) GetParameters() (map[string]interface{}, error) {
var params = map[string]interface{}{}
// check market field -> json key market
market := g.market
// assign parameter of market
params["market"] = market
// check fromID field -> json key from_id
if g.fromID != nil {
fromID := *g.fromID
// assign parameter of fromID
params["from_id"] = fromID
} else {
}
// check limit field -> json key limit
if g.limit != nil {
limit := *g.limit
// assign parameter of limit
params["limit"] = limit
} else {
}
return params, nil
}
// GetParametersQuery converts the parameters from GetParameters into the url.Values format
func (g *GetOrderHistoryRequest) GetParametersQuery() (url.Values, error) {
query := url.Values{}
params, err := g.GetParameters()
if err != nil {
return query, err
}
for _k, _v := range params {
if g.isVarSlice(_v) {
g.iterateSlice(_v, func(it interface{}) {
query.Add(_k+"[]", fmt.Sprintf("%v", it))
})
} else {
query.Add(_k, fmt.Sprintf("%v", _v))
}
}
return query, nil
}
// GetParametersJSON converts the parameters from GetParameters into the JSON format
func (g *GetOrderHistoryRequest) GetParametersJSON() ([]byte, error) {
params, err := g.GetParameters()
if err != nil {
return nil, err
}
return json.Marshal(params)
}
// GetSlugParameters builds and checks the slug parameters and return the result in a map object
func (g *GetOrderHistoryRequest) GetSlugParameters() (map[string]interface{}, error) {
var params = map[string]interface{}{}
return params, nil
}
func (g *GetOrderHistoryRequest) applySlugsToUrl(url string, slugs map[string]string) string {
for _k, _v := range slugs {
needleRE := regexp.MustCompile(":" + _k + "\\b")
url = needleRE.ReplaceAllString(url, _v)
}
return url
}
func (g *GetOrderHistoryRequest) iterateSlice(slice interface{}, _f func(it interface{})) {
sliceValue := reflect.ValueOf(slice)
for _i := 0; _i < sliceValue.Len(); _i++ {
it := sliceValue.Index(_i).Interface()
_f(it)
}
}
func (g *GetOrderHistoryRequest) isVarSlice(_v interface{}) bool {
rt := reflect.TypeOf(_v)
switch rt.Kind() {
case reflect.Slice:
return true
}
return false
}
func (g *GetOrderHistoryRequest) GetSlugsMap() (map[string]string, error) {
slugs := map[string]string{}
params, err := g.GetSlugParameters()
if err != nil {
return slugs, nil
}
for _k, _v := range params {
slugs[_k] = fmt.Sprintf("%v", _v)
}
return slugs, nil
}
func (g *GetOrderHistoryRequest) Do(ctx context.Context) ([]Order, error) {
// empty params for GET operation
var params interface{}
query, err := g.GetParametersQuery()
if err != nil {
return nil, err
}
apiURL := "v2/orders/history"
req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params)
if err != nil {
return nil, err
}
response, err := g.client.SendRequest(req)
if err != nil {
return nil, err
}
var apiResponse []Order
if err := response.DecodeJSON(&apiResponse); err != nil {
return nil, err
}
return apiResponse, nil
}

View File

@ -1,242 +0,0 @@
// Code generated by "requestgen -method GET -url v2/trades/my -type GetPrivateTradesRequest -responseType []Trade"; DO NOT EDIT.
package max
import (
"context"
"encoding/json"
"fmt"
"net/url"
"reflect"
"regexp"
"strconv"
"time"
)
func (p *GetPrivateTradesRequest) Market(market string) *GetPrivateTradesRequest {
p.market = market
return p
}
func (p *GetPrivateTradesRequest) Timestamp(timestamp time.Time) *GetPrivateTradesRequest {
p.timestamp = &timestamp
return p
}
func (p *GetPrivateTradesRequest) From(from int64) *GetPrivateTradesRequest {
p.from = &from
return p
}
func (p *GetPrivateTradesRequest) To(to int64) *GetPrivateTradesRequest {
p.to = &to
return p
}
func (p *GetPrivateTradesRequest) OrderBy(orderBy string) *GetPrivateTradesRequest {
p.orderBy = &orderBy
return p
}
func (p *GetPrivateTradesRequest) Pagination(pagination bool) *GetPrivateTradesRequest {
p.pagination = &pagination
return p
}
func (p *GetPrivateTradesRequest) Limit(limit int64) *GetPrivateTradesRequest {
p.limit = &limit
return p
}
func (p *GetPrivateTradesRequest) Offset(offset int64) *GetPrivateTradesRequest {
p.offset = &offset
return p
}
// GetQueryParameters builds and checks the query parameters and returns url.Values
func (p *GetPrivateTradesRequest) GetQueryParameters() (url.Values, error) {
var params = map[string]interface{}{}
query := url.Values{}
for k, v := range params {
query.Add(k, fmt.Sprintf("%v", v))
}
return query, nil
}
// GetParameters builds and checks the parameters and return the result in a map object
func (p *GetPrivateTradesRequest) GetParameters() (map[string]interface{}, error) {
var params = map[string]interface{}{}
// check market field -> json key market
market := p.market
// assign parameter of market
params["market"] = market
// check timestamp field -> json key timestamp
if p.timestamp != nil {
timestamp := *p.timestamp
// assign parameter of timestamp
// convert time.Time to seconds time stamp
params["timestamp"] = strconv.FormatInt(timestamp.Unix(), 10)
} else {
}
// check from field -> json key from
if p.from != nil {
from := *p.from
// assign parameter of from
params["from"] = from
} else {
}
// check to field -> json key to
if p.to != nil {
to := *p.to
// assign parameter of to
params["to"] = to
} else {
}
// check orderBy field -> json key order_by
if p.orderBy != nil {
orderBy := *p.orderBy
// assign parameter of orderBy
params["order_by"] = orderBy
} else {
}
// check pagination field -> json key pagination
if p.pagination != nil {
pagination := *p.pagination
// assign parameter of pagination
params["pagination"] = pagination
} else {
}
// check limit field -> json key limit
if p.limit != nil {
limit := *p.limit
// assign parameter of limit
params["limit"] = limit
} else {
}
// check offset field -> json key offset
if p.offset != nil {
offset := *p.offset
// assign parameter of offset
params["offset"] = offset
} else {
}
return params, nil
}
// GetParametersQuery converts the parameters from GetParameters into the url.Values format
func (p *GetPrivateTradesRequest) GetParametersQuery() (url.Values, error) {
query := url.Values{}
params, err := p.GetParameters()
if err != nil {
return query, err
}
for k, v := range params {
if p.isVarSlice(v) {
p.iterateSlice(v, func(it interface{}) {
query.Add(k+"[]", fmt.Sprintf("%v", it))
})
} else {
query.Add(k, fmt.Sprintf("%v", v))
}
}
return query, nil
}
// GetParametersJSON converts the parameters from GetParameters into the JSON format
func (p *GetPrivateTradesRequest) GetParametersJSON() ([]byte, error) {
params, err := p.GetParameters()
if err != nil {
return nil, err
}
return json.Marshal(params)
}
// GetSlugParameters builds and checks the slug parameters and return the result in a map object
func (p *GetPrivateTradesRequest) GetSlugParameters() (map[string]interface{}, error) {
var params = map[string]interface{}{}
return params, nil
}
func (p *GetPrivateTradesRequest) applySlugsToUrl(url string, slugs map[string]string) string {
for k, v := range slugs {
needleRE := regexp.MustCompile(":" + k + "\\b")
url = needleRE.ReplaceAllString(url, v)
}
return url
}
func (p *GetPrivateTradesRequest) iterateSlice(slice interface{}, f func(it interface{})) {
sliceValue := reflect.ValueOf(slice)
for i := 0; i < sliceValue.Len(); i++ {
it := sliceValue.Index(i).Interface()
f(it)
}
}
func (p *GetPrivateTradesRequest) isVarSlice(v interface{}) bool {
rt := reflect.TypeOf(v)
switch rt.Kind() {
case reflect.Slice:
return true
}
return false
}
func (p *GetPrivateTradesRequest) GetSlugsMap() (map[string]string, error) {
slugs := map[string]string{}
params, err := p.GetSlugParameters()
if err != nil {
return slugs, nil
}
for k, v := range params {
slugs[k] = fmt.Sprintf("%v", v)
}
return slugs, nil
}
func (p *GetPrivateTradesRequest) Do(ctx context.Context) ([]Trade, error) {
// empty params for GET operation
var params interface{}
query, err := p.GetParametersQuery()
if err != nil {
return nil, err
}
apiURL := "v2/trades/my"
req, err := p.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params)
if err != nil {
return nil, err
}
response, err := p.client.SendRequest(req)
if err != nil {
return nil, err
}
var apiResponse []Trade
if err := response.DecodeJSON(&apiResponse); err != nil {
return nil, err
}
return apiResponse, nil
}

View File

@ -6,7 +6,6 @@ package max
import ( import (
"context" "context"
"net/url" "net/url"
"time"
"github.com/c9s/requestgen" "github.com/c9s/requestgen"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -99,7 +98,7 @@ type SubmitOrder struct {
// Order represents one returned order (POST order/GET order/GET orders) on the max platform. // Order represents one returned order (POST order/GET order/GET orders) on the max platform.
type Order struct { type Order struct {
ID uint64 `json:"id,omitempty"` ID uint64 `json:"id,omitempty"`
WalletType string `json:"wallet_type,omitempty"` WalletType WalletType `json:"wallet_type,omitempty"`
Side string `json:"side"` Side string `json:"side"`
OrderType OrderType `json:"ord_type"` OrderType OrderType `json:"ord_type"`
Price fixedpoint.Value `json:"price,omitempty"` Price fixedpoint.Value `json:"price,omitempty"`
@ -113,64 +112,7 @@ type Order struct {
TradesCount int64 `json:"trades_count,omitempty"` TradesCount int64 `json:"trades_count,omitempty"`
GroupID uint32 `json:"group_id,omitempty"` GroupID uint32 `json:"group_id,omitempty"`
ClientOID string `json:"client_oid,omitempty"` ClientOID string `json:"client_oid,omitempty"`
CreatedAt time.Time `json:"-"` CreatedAt types.MillisecondTimestamp `json:"created_at"`
CreatedAtMs types.MillisecondTimestamp `json:"created_at_in_ms,omitempty"`
InsertedAt time.Time `json:"-"`
}
// Open returns open orders
func (s *OrderService) Closed(market string, options QueryOrderOptions) ([]Order, error) {
req := s.NewGetOrdersRequest()
req.Market(market)
req.State([]OrderState{OrderStateDone, OrderStateCancel})
if options.GroupID > 0 {
req.GroupID(uint32(options.GroupID))
}
if options.Offset > 0 {
req.Offset(options.Offset)
}
if options.Limit > 0 {
req.Limit(options.Limit)
}
if options.Page > 0 {
req.Page(options.Page)
}
if len(options.OrderBy) > 0 {
req.OrderBy(options.OrderBy)
}
return req.Do(context.Background())
}
// Open returns open orders
func (s *OrderService) Open(market string, options QueryOrderOptions) ([]Order, error) {
req := s.NewGetOrdersRequest()
req.Market(market)
// state default ot wait and convert
if options.GroupID > 0 {
req.GroupID(uint32(options.GroupID))
}
return req.Do(context.Background())
}
//go:generate GetRequest -url "v2/orders/history" -type GetOrderHistoryRequest -responseType []Order
type GetOrderHistoryRequest struct {
client requestgen.AuthenticatedAPIClient
market string `param:"market"`
fromID *uint64 `param:"from_id"`
limit *uint `param:"limit"`
}
func (s *OrderService) NewGetOrderHistoryRequest() *GetOrderHistoryRequest {
return &GetOrderHistoryRequest{
client: s.client,
}
} }
//go:generate GetRequest -url "v2/orders" -type GetOrdersRequest -responseType []Order //go:generate GetRequest -url "v2/orders" -type GetOrdersRequest -responseType []Order

View File

@ -73,24 +73,6 @@ func TestOrderService_GetOrdersRequest_SingleState(t *testing.T) {
assert.NotNil(t, orders) assert.NotNil(t, orders)
} }
func TestOrderService_GetOrderHistoryRequest(t *testing.T) {
key, secret, ok := integrationTestConfigured(t, "MAX")
if !ok {
t.SkipNow()
}
ctx := context.Background()
client := NewRestClient(ProductionAPIURL)
client.Auth(key, secret)
req := client.OrderService.NewGetOrderHistoryRequest()
req.Market("btcusdt")
req.FromID(1)
orders, err := req.Do(ctx)
assert.NoError(t, err)
assert.NotNil(t, orders)
}
func TestOrderService(t *testing.T) { func TestOrderService(t *testing.T) {
key, secret, ok := integrationTestConfigured(t, "MAX") key, secret, ok := integrationTestConfigured(t, "MAX")

View File

@ -10,6 +10,7 @@ import (
"github.com/c9s/requestgen" "github.com/c9s/requestgen"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/types"
) )
@ -26,20 +27,23 @@ type TradeInfo struct {
Ask *MarkerInfo `json:"ask,omitempty"` Ask *MarkerInfo `json:"ask,omitempty"`
} }
type Liquidity string
// Trade represents one returned trade on the max platform. // Trade represents one returned trade on the max platform.
type Trade struct { type Trade struct {
ID uint64 `json:"id" db:"exchange_id"` ID uint64 `json:"id" db:"exchange_id"`
Price string `json:"price" db:"price"` WalletType WalletType `json:"wallet_type,omitempty"`
Volume string `json:"volume" db:"volume"` Price fixedpoint.Value `json:"price"`
Funds string `json:"funds"` Volume fixedpoint.Value `json:"volume"`
Market string `json:"market" db:"market"` Funds fixedpoint.Value `json:"funds"`
Market string `json:"market"`
MarketName string `json:"market_name"` MarketName string `json:"market_name"`
CreatedAt int64 `json:"created_at"` CreatedAt types.MillisecondTimestamp `json:"created_at"`
CreatedAtMilliSeconds types.MillisecondTimestamp `json:"created_at_in_ms"` Side string `json:"side"`
Side string `json:"side" db:"side"`
OrderID uint64 `json:"order_id"` OrderID uint64 `json:"order_id"`
Fee string `json:"fee" db:"fee"` // float number as string Fee fixedpoint.Value `json:"fee"` // float number as string
FeeCurrency string `json:"fee_currency" db:"fee_currency"` FeeCurrency string `json:"fee_currency"`
Liquidity Liquidity `json:"liquidity"`
Info TradeInfo `json:"info,omitempty"` Info TradeInfo `json:"info,omitempty"`
} }
@ -148,4 +152,3 @@ type GetPrivateTradesRequest struct {
offset *int64 `param:"offset"` offset *int64 `param:"offset"`
} }

View File

@ -1,52 +0,0 @@
package max
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestTradeService(t *testing.T) {
key, secret, ok := integrationTestConfigured(t, "MAX")
if !ok {
t.SkipNow()
}
ctx := context.Background()
client := NewRestClient(ProductionAPIURL)
client.Auth(key, secret)
t.Run("default timestamp", func(t *testing.T) {
req := client.TradeService.NewGetPrivateTradeRequest()
until := time.Now().AddDate(0, -6, 0)
trades, err := req.Market("btcusdt").
Timestamp(until).
Do(ctx)
if assert.NoError(t, err) {
assert.NotEmptyf(t, trades, "got %d trades", len(trades))
for _, td := range trades {
t.Logf("trade: %+v", td)
assert.True(t, td.CreatedAtMilliSeconds.Time().Before(until))
}
}
})
t.Run("desc and pagination = false", func(t *testing.T) {
req := client.TradeService.NewGetPrivateTradeRequest()
trades, err := req.Market("btcusdt").
Pagination(false).
OrderBy("asc").
Do(ctx)
if assert.NoError(t, err) {
assert.NotEmptyf(t, trades, "got %d trades", len(trades))
for _, td := range trades {
t.Logf("trade: %+v", td)
}
}
})
}

View File

@ -6,6 +6,7 @@ import (
"strings" "strings"
"github.com/pkg/errors" "github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"github.com/valyala/fastjson" "github.com/valyala/fastjson"
"github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/fixedpoint"
@ -23,16 +24,16 @@ type OrderUpdate struct {
Side string `json:"sd"` Side string `json:"sd"`
OrderType OrderType `json:"ot"` OrderType OrderType `json:"ot"`
Price string `json:"p"` Price fixedpoint.Value `json:"p"`
StopPrice string `json:"sp"` StopPrice fixedpoint.Value `json:"sp"`
Volume string `json:"v"` Volume fixedpoint.Value `json:"v"`
AveragePrice string `json:"ap"` AveragePrice fixedpoint.Value `json:"ap"`
State OrderState `json:"S"` State OrderState `json:"S"`
Market string `json:"M"` Market string `json:"M"`
RemainingVolume string `json:"rv"` RemainingVolume fixedpoint.Value `json:"rv"`
ExecutedVolume string `json:"ev"` ExecutedVolume fixedpoint.Value `json:"ev"`
TradesCount int64 `json:"tc"` TradesCount int64 `json:"tc"`
@ -48,36 +49,20 @@ type OrderUpdateEvent struct {
Orders []OrderUpdate `json:"o"` Orders []OrderUpdate `json:"o"`
} }
func parserOrderUpdate(v *fastjson.Value) OrderUpdate {
return OrderUpdate{
Event: string(v.GetStringBytes("e")),
ID: v.GetUint64("i"),
Side: string(v.GetStringBytes("sd")),
Market: string(v.GetStringBytes("M")),
OrderType: OrderType(v.GetStringBytes("ot")),
State: OrderState(v.GetStringBytes("S")),
Price: string(v.GetStringBytes("p")),
StopPrice: string(v.GetStringBytes("sp")),
AveragePrice: string(v.GetStringBytes("ap")),
Volume: string(v.GetStringBytes("v")),
RemainingVolume: string(v.GetStringBytes("rv")),
ExecutedVolume: string(v.GetStringBytes("ev")),
TradesCount: v.GetInt64("tc"),
GroupID: uint32(v.GetInt("gi")),
ClientOID: string(v.GetStringBytes("ci")),
CreatedAtMs: v.GetInt64("T"),
UpdateTime: v.GetInt64("TU"),
}
}
func parseOrderUpdateEvent(v *fastjson.Value) *OrderUpdateEvent { func parseOrderUpdateEvent(v *fastjson.Value) *OrderUpdateEvent {
var e OrderUpdateEvent var e OrderUpdateEvent
e.Event = string(v.GetStringBytes("e")) e.Event = string(v.GetStringBytes("e"))
e.Timestamp = v.GetInt64("T") e.Timestamp = v.GetInt64("T")
for _, ov := range v.GetArray("o") { for _, ov := range v.GetArray("o") {
o := parserOrderUpdate(ov) var o = ov.String()
e.Orders = append(e.Orders, o) var u OrderUpdate
if err := json.Unmarshal([]byte(o), &u); err != nil {
log.WithError(err).Error("parse error")
continue
}
e.Orders = append(e.Orders, u)
} }
return &e return &e
@ -95,8 +80,14 @@ func parserOrderSnapshotEvent(v *fastjson.Value) *OrderSnapshotEvent {
e.Timestamp = v.GetInt64("T") e.Timestamp = v.GetInt64("T")
for _, ov := range v.GetArray("o") { for _, ov := range v.GetArray("o") {
o := parserOrderUpdate(ov) var o = ov.String()
e.Orders = append(e.Orders, o) var u OrderUpdate
if err := json.Unmarshal([]byte(o), &u); err != nil {
log.WithError(err).Error("parse error")
continue
}
e.Orders = append(e.Orders, u)
} }
return &e return &e

View File

@ -5,6 +5,8 @@ package v3
//go:generate -command DeleteRequest requestgen -method DELETE //go:generate -command DeleteRequest requestgen -method DELETE
import ( import (
"time"
"github.com/c9s/requestgen" "github.com/c9s/requestgen"
maxapi "github.com/c9s/bbgo/pkg/exchange/max/maxapi" maxapi "github.com/c9s/bbgo/pkg/exchange/max/maxapi"
@ -35,6 +37,10 @@ func (s *OrderService) NewWalletOrderCancelAllRequest(walletType WalletType) *Wa
return &WalletOrderCancelAllRequest{client: s.Client, walletType: walletType} return &WalletOrderCancelAllRequest{client: s.Client, walletType: walletType}
} }
func (s *OrderService) NewWalletGetTradesRequest(walletType WalletType) *WalletGetTradesRequest {
return &WalletGetTradesRequest{client: s.Client, walletType: walletType}
}
func (s *OrderService) NewOrderCancelRequest() *OrderCancelRequest { func (s *OrderService) NewOrderCancelRequest() *OrderCancelRequest {
return &OrderCancelRequest{client: s.Client} return &OrderCancelRequest{client: s.Client}
} }
@ -88,6 +94,21 @@ type WalletOrderCancelAllRequest struct {
groupID *uint32 `param:"groupID"` groupID *uint32 `param:"groupID"`
} }
type Trade = maxapi.Trade
//go:generate GetRequest -url "/api/v3/wallet/:walletType/trades" -type WalletGetTradesRequest -responseType []Trade
type WalletGetTradesRequest struct {
client requestgen.AuthenticatedAPIClient
walletType WalletType `param:"walletType,slug,required"`
market string `param:"market,required"`
from *uint64 `param:"from_id"`
startTime *time.Time `param:"start_time,milliseconds"`
endTime *time.Time `param:"end_time,milliseconds"`
limit *uint64 `param:"limit"`
}
//go:generate PostRequest -url "/api/v3/order" -type OrderCancelRequest -responseType .Order //go:generate PostRequest -url "/api/v3/order" -type OrderCancelRequest -responseType .Order
type OrderCancelRequest struct { type OrderCancelRequest struct {
client requestgen.AuthenticatedAPIClient client requestgen.AuthenticatedAPIClient

View File

@ -0,0 +1,233 @@
// Code generated by "requestgen -method GET -url /api/v3/wallet/:walletType/trades -type WalletGetTradesRequest -responseType []Trade"; DO NOT EDIT.
package v3
import (
"context"
"encoding/json"
"fmt"
"github.com/c9s/bbgo/pkg/exchange/max/maxapi"
"net/url"
"reflect"
"regexp"
"strconv"
"time"
)
func (w *WalletGetTradesRequest) Market(market string) *WalletGetTradesRequest {
w.market = market
return w
}
func (w *WalletGetTradesRequest) From(from uint64) *WalletGetTradesRequest {
w.from = &from
return w
}
func (w *WalletGetTradesRequest) StartTime(startTime time.Time) *WalletGetTradesRequest {
w.startTime = &startTime
return w
}
func (w *WalletGetTradesRequest) EndTime(endTime time.Time) *WalletGetTradesRequest {
w.endTime = &endTime
return w
}
func (w *WalletGetTradesRequest) Limit(limit uint64) *WalletGetTradesRequest {
w.limit = &limit
return w
}
func (w *WalletGetTradesRequest) WalletType(walletType max.WalletType) *WalletGetTradesRequest {
w.walletType = walletType
return w
}
// GetQueryParameters builds and checks the query parameters and returns url.Values
func (w *WalletGetTradesRequest) GetQueryParameters() (url.Values, error) {
var params = map[string]interface{}{}
query := url.Values{}
for _k, _v := range params {
query.Add(_k, fmt.Sprintf("%v", _v))
}
return query, nil
}
// GetParameters builds and checks the parameters and return the result in a map object
func (w *WalletGetTradesRequest) GetParameters() (map[string]interface{}, error) {
var params = map[string]interface{}{}
// check market field -> json key market
market := w.market
// TEMPLATE check-required
if len(market) == 0 {
return nil, fmt.Errorf("market is required, empty string given")
}
// END TEMPLATE check-required
// assign parameter of market
params["market"] = market
// check from field -> json key from_id
if w.from != nil {
from := *w.from
// assign parameter of from
params["from_id"] = from
} else {
}
// check startTime field -> json key start_time
if w.startTime != nil {
startTime := *w.startTime
// assign parameter of startTime
// convert time.Time to milliseconds time stamp
params["start_time"] = strconv.FormatInt(startTime.UnixNano()/int64(time.Millisecond), 10)
} else {
}
// check endTime field -> json key end_time
if w.endTime != nil {
endTime := *w.endTime
// assign parameter of endTime
// convert time.Time to milliseconds time stamp
params["end_time"] = strconv.FormatInt(endTime.UnixNano()/int64(time.Millisecond), 10)
} else {
}
// check limit field -> json key limit
if w.limit != nil {
limit := *w.limit
// assign parameter of limit
params["limit"] = limit
} else {
}
return params, nil
}
// GetParametersQuery converts the parameters from GetParameters into the url.Values format
func (w *WalletGetTradesRequest) GetParametersQuery() (url.Values, error) {
query := url.Values{}
params, err := w.GetParameters()
if err != nil {
return query, err
}
for _k, _v := range params {
if w.isVarSlice(_v) {
w.iterateSlice(_v, func(it interface{}) {
query.Add(_k+"[]", fmt.Sprintf("%v", it))
})
} else {
query.Add(_k, fmt.Sprintf("%v", _v))
}
}
return query, nil
}
// GetParametersJSON converts the parameters from GetParameters into the JSON format
func (w *WalletGetTradesRequest) GetParametersJSON() ([]byte, error) {
params, err := w.GetParameters()
if err != nil {
return nil, err
}
return json.Marshal(params)
}
// GetSlugParameters builds and checks the slug parameters and return the result in a map object
func (w *WalletGetTradesRequest) GetSlugParameters() (map[string]interface{}, error) {
var params = map[string]interface{}{}
// check walletType field -> json key walletType
walletType := w.walletType
// TEMPLATE check-required
if len(walletType) == 0 {
return nil, fmt.Errorf("walletType is required, empty string given")
}
// END TEMPLATE check-required
// assign parameter of walletType
params["walletType"] = walletType
return params, nil
}
func (w *WalletGetTradesRequest) applySlugsToUrl(url string, slugs map[string]string) string {
for _k, _v := range slugs {
needleRE := regexp.MustCompile(":" + _k + "\\b")
url = needleRE.ReplaceAllString(url, _v)
}
return url
}
func (w *WalletGetTradesRequest) iterateSlice(slice interface{}, _f func(it interface{})) {
sliceValue := reflect.ValueOf(slice)
for _i := 0; _i < sliceValue.Len(); _i++ {
it := sliceValue.Index(_i).Interface()
_f(it)
}
}
func (w *WalletGetTradesRequest) isVarSlice(_v interface{}) bool {
rt := reflect.TypeOf(_v)
switch rt.Kind() {
case reflect.Slice:
return true
}
return false
}
func (w *WalletGetTradesRequest) GetSlugsMap() (map[string]string, error) {
slugs := map[string]string{}
params, err := w.GetSlugParameters()
if err != nil {
return slugs, nil
}
for _k, _v := range params {
slugs[_k] = fmt.Sprintf("%v", _v)
}
return slugs, nil
}
func (w *WalletGetTradesRequest) Do(ctx context.Context) ([]max.Trade, error) {
// empty params for GET operation
var params interface{}
query, err := w.GetParametersQuery()
if err != nil {
return nil, err
}
apiURL := "/api/v3/wallet/:walletType/trades"
slugs, err := w.GetSlugsMap()
if err != nil {
return nil, err
}
apiURL = w.applySlugsToUrl(apiURL, slugs)
req, err := w.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params)
if err != nil {
return nil, err
}
response, err := w.client.SendRequest(req)
if err != nil {
return nil, err
}
var apiResponse []max.Trade
if err := response.DecodeJSON(&apiResponse); err != nil {
return nil, err
}
return apiResponse, nil
}