2020-08-31 04:32:51 +00:00
package max
2020-10-05 10:26:31 +00:00
import (
"context"
2020-11-09 08:34:35 +00:00
"fmt"
2020-10-28 09:44:37 +00:00
"math"
2020-12-17 06:44:30 +00:00
"os"
2021-02-23 08:39:48 +00:00
"sort"
2022-02-10 12:39:20 +00:00
"strconv"
2020-10-06 10:44:56 +00:00
"time"
2020-10-05 10:26:31 +00:00
2020-10-10 09:50:49 +00:00
"github.com/pkg/errors"
2020-10-17 02:39:03 +00:00
"github.com/sirupsen/logrus"
2022-01-26 15:51:23 +00:00
"go.uber.org/multierr"
2021-02-22 05:36:39 +00:00
"golang.org/x/time/rate"
2020-10-10 09:50:49 +00:00
2020-10-11 08:46:15 +00:00
maxapi "github.com/c9s/bbgo/pkg/exchange/max/maxapi"
2022-05-26 11:52:38 +00:00
v3 "github.com/c9s/bbgo/pkg/exchange/max/maxapi/v3"
2020-11-10 06:19:33 +00:00
"github.com/c9s/bbgo/pkg/fixedpoint"
2020-10-11 08:46:15 +00:00
"github.com/c9s/bbgo/pkg/types"
2020-10-05 10:26:31 +00:00
)
2020-10-17 02:39:03 +00:00
var log = logrus . WithField ( "exchange" , "max" )
2020-10-05 10:26:31 +00:00
2020-08-31 04:32:51 +00:00
type Exchange struct {
2022-05-25 06:38:09 +00:00
types . MarginSettings
2020-10-05 10:26:31 +00:00
key , secret string
2022-05-26 11:52:38 +00:00
client * maxapi . RestClient
2023-05-04 06:37:19 +00:00
v3client * v3 . Client
2022-05-27 11:46:28 +00:00
v3margin * v3 . MarginService
2023-03-21 08:25:16 +00:00
2023-04-12 14:56:23 +00:00
submitOrderLimiter , queryTradeLimiter , accountQueryLimiter , closedOrderQueryLimiter , marketDataLimiter * rate . Limiter
2020-08-31 04:32:51 +00:00
}
2020-10-05 10:26:31 +00:00
func New ( key , secret string ) * Exchange {
2020-12-17 06:44:30 +00:00
baseURL := maxapi . ProductionAPIURL
2020-12-29 08:00:03 +00:00
if override := os . Getenv ( "MAX_API_BASE_URL" ) ; len ( override ) > 0 {
2020-12-17 06:44:30 +00:00
baseURL = override
}
client := maxapi . NewRestClient ( baseURL )
2020-10-05 10:26:31 +00:00
client . Auth ( key , secret )
2020-08-31 04:32:51 +00:00
return & Exchange {
2022-06-17 07:04:23 +00:00
client : client ,
key : key ,
// pragma: allowlist nextline secret
2022-05-27 11:46:28 +00:00
secret : secret ,
2023-05-04 06:37:19 +00:00
v3client : & v3 . Client { Client : client } ,
2022-05-27 11:46:28 +00:00
v3margin : & v3 . MarginService { Client : client } ,
2023-03-21 08:25:16 +00:00
2023-03-21 08:26:47 +00:00
queryTradeLimiter : rate . NewLimiter ( rate . Every ( 1 * time . Second ) , 2 ) ,
2023-03-21 08:25:16 +00:00
submitOrderLimiter : rate . NewLimiter ( rate . Every ( 100 * time . Millisecond ) , 10 ) ,
2023-04-12 14:56:23 +00:00
// closedOrderQueryLimiter is used for the closed orders query rate limit, 1 request per second
closedOrderQueryLimiter : rate . NewLimiter ( rate . Every ( 1 * time . Second ) , 1 ) ,
2023-04-12 14:58:10 +00:00
accountQueryLimiter : rate . NewLimiter ( rate . Every ( 1 * time . Second ) , 1 ) ,
2023-04-12 14:56:23 +00:00
marketDataLimiter : rate . NewLimiter ( rate . Every ( 2 * time . Second ) , 10 ) ,
2020-10-05 10:26:31 +00:00
}
}
2020-10-11 12:08:54 +00:00
func ( e * Exchange ) Name ( ) types . ExchangeName {
return types . ExchangeMax
}
2021-02-18 09:37:49 +00:00
func ( e * Exchange ) QueryTicker ( ctx context . Context , symbol string ) ( * types . Ticker , error ) {
ticker , err := e . client . PublicService . Ticker ( toLocalSymbol ( symbol ) )
if err != nil {
return nil , err
}
return & types . Ticker {
Time : ticker . Time ,
2023-04-12 06:56:33 +00:00
Volume : ticker . Volume ,
Last : ticker . Last ,
Open : ticker . Open ,
High : ticker . High ,
Low : ticker . Low ,
Buy : ticker . Buy ,
Sell : ticker . Sell ,
2021-02-18 09:37:49 +00:00
} , nil
}
2021-02-06 17:35:23 +00:00
func ( e * Exchange ) QueryTickers ( ctx context . Context , symbol ... string ) ( map [ string ] types . Ticker , error ) {
2023-04-12 14:56:23 +00:00
if err := e . marketDataLimiter . Wait ( ctx ) ; err != nil {
2021-02-22 07:01:05 +00:00
return nil , err
}
2021-02-06 17:35:23 +00:00
2021-02-22 07:01:05 +00:00
var tickers = make ( map [ string ] types . Ticker )
2021-02-06 17:35:23 +00:00
if len ( symbol ) == 1 {
2021-02-18 09:37:49 +00:00
ticker , err := e . QueryTicker ( ctx , symbol [ 0 ] )
2021-02-06 17:35:23 +00:00
if err != nil {
return nil , err
}
2021-02-18 09:37:49 +00:00
2021-02-22 07:01:05 +00:00
tickers [ toGlobalSymbol ( symbol [ 0 ] ) ] = * ticker
2021-02-06 17:35:23 +00:00
} else {
2023-04-13 08:40:07 +00:00
req := e . client . NewGetTickersRequest ( )
maxTickers , err := req . Do ( ctx )
2021-02-06 17:35:23 +00:00
if err != nil {
return nil , err
}
2021-02-07 21:58:30 +00:00
m := make ( map [ string ] struct { } )
exists := struct { } { }
2021-02-06 17:35:23 +00:00
for _ , s := range symbol {
2021-02-07 21:58:30 +00:00
m [ toGlobalSymbol ( s ) ] = exists
2021-02-06 17:35:23 +00:00
}
2021-02-22 07:01:05 +00:00
for k , v := range maxTickers {
2021-02-07 21:58:30 +00:00
if _ , ok := m [ toGlobalSymbol ( k ) ] ; len ( symbol ) != 0 && ! ok {
2021-02-06 17:35:23 +00:00
continue
}
2023-04-12 06:56:33 +00:00
2021-02-22 07:01:05 +00:00
tickers [ toGlobalSymbol ( k ) ] = types . Ticker {
2021-02-06 17:35:23 +00:00
Time : v . Time ,
2023-04-12 06:56:33 +00:00
Volume : v . Volume ,
Last : v . Last ,
Open : v . Open ,
High : v . High ,
Low : v . Low ,
Buy : v . Buy ,
Sell : v . Sell ,
2021-02-06 17:35:23 +00:00
}
}
}
2021-02-22 07:01:05 +00:00
return tickers , nil
2021-02-04 21:48:35 +00:00
}
2020-10-14 02:53:18 +00:00
func ( e * Exchange ) QueryMarkets ( ctx context . Context ) ( types . MarketMap , error ) {
2023-04-13 08:40:07 +00:00
req := e . client . NewGetMarketsRequest ( )
remoteMarkets , err := req . Do ( ctx )
2020-10-14 02:53:18 +00:00
if err != nil {
return nil , err
}
markets := types . MarketMap { }
for _ , m := range remoteMarkets {
2020-10-25 10:26:10 +00:00
symbol := toGlobalSymbol ( m . ID )
2020-10-14 02:53:18 +00:00
market := types . Market {
2020-10-25 10:26:10 +00:00
Symbol : symbol ,
2021-05-25 18:13:59 +00:00
LocalSymbol : m . ID ,
2020-10-14 02:53:18 +00:00
PricePrecision : m . QuoteUnitPrecision ,
VolumePrecision : m . BaseUnitPrecision ,
QuoteCurrency : toGlobalCurrency ( m . QuoteUnit ) ,
BaseCurrency : toGlobalCurrency ( m . BaseUnit ) ,
MinNotional : m . MinQuoteAmount ,
MinAmount : m . MinQuoteAmount ,
2021-02-10 16:21:56 +00:00
MinQuantity : m . MinBaseAmount ,
2022-02-03 04:55:25 +00:00
MaxQuantity : fixedpoint . NewFromInt ( 10000 ) ,
// make it like 0.0001
StepSize : fixedpoint . NewFromFloat ( 1.0 / math . Pow10 ( m . BaseUnitPrecision ) ) ,
2022-03-07 05:56:20 +00:00
// used in the price formatter
2022-02-03 04:55:25 +00:00
MinPrice : fixedpoint . NewFromFloat ( 1.0 / math . Pow10 ( m . QuoteUnitPrecision ) ) ,
MaxPrice : fixedpoint . NewFromInt ( 10000 ) ,
TickSize : fixedpoint . NewFromFloat ( 1.0 / math . Pow10 ( m . QuoteUnitPrecision ) ) ,
2020-10-14 02:53:18 +00:00
}
2020-10-25 10:26:10 +00:00
markets [ symbol ] = market
2020-10-14 02:53:18 +00:00
}
return markets , nil
}
2020-10-05 10:26:31 +00:00
func ( e * Exchange ) NewStream ( ) types . Stream {
2022-05-25 06:38:09 +00:00
stream := NewStream ( e . key , e . secret )
stream . MarginSettings = e . MarginSettings
return stream
2020-10-05 10:26:31 +00:00
}
2022-08-05 03:20:55 +00:00
func ( e * Exchange ) QueryOrderTrades ( ctx context . Context , q types . OrderQuery ) ( [ ] types . Trade , error ) {
2022-08-05 03:29:59 +00:00
if q . OrderID == "" {
return nil , errors . New ( "max.QueryOrder: OrderID is required parameter" )
}
2022-08-05 03:20:55 +00:00
orderID , err := strconv . ParseInt ( q . OrderID , 10 , 64 )
if err != nil {
return nil , err
}
2023-05-04 06:37:19 +00:00
maxTrades , err := e . v3client . NewGetOrderTradesRequest ( ) . OrderID ( uint64 ( orderID ) ) . Do ( ctx )
2022-08-05 03:20:55 +00:00
if err != nil {
return nil , err
}
var trades [ ] types . Trade
for _ , t := range maxTrades {
2023-03-08 09:18:18 +00:00
localTrades , err := toGlobalTradeV3 ( t )
2022-08-05 03:20:55 +00:00
if err != nil {
log . WithError ( err ) . Errorf ( "can not convert trade: %+v" , t )
continue
}
2023-03-09 08:15:38 +00:00
// because self-trades will contains ask and bid orders in its struct
// we need to make sure the trade's order is what we want
for _ , localTrade := range localTrades {
if localTrade . OrderID == uint64 ( orderID ) {
trades = append ( trades , localTrade )
}
}
2022-08-05 03:20:55 +00:00
}
// ensure everything is sorted ascending
trades = types . SortTradesAscending ( trades )
return trades , nil
}
2022-02-10 09:48:53 +00:00
func ( e * Exchange ) QueryOrder ( ctx context . Context , q types . OrderQuery ) ( * types . Order , error ) {
2023-04-14 08:44:56 +00:00
if len ( q . OrderID ) == 0 && len ( q . ClientOrderID ) == 0 {
return nil , errors . New ( "max.QueryOrder: one of OrderID/ClientOrderID is required parameter" )
2022-08-05 03:29:59 +00:00
}
2023-04-14 08:44:56 +00:00
if len ( q . OrderID ) != 0 && len ( q . ClientOrderID ) != 0 {
return nil , errors . New ( "max.QueryOrder: only accept one parameter of OrderID/ClientOrderID" )
}
2023-05-04 06:37:19 +00:00
request := e . v3client . NewGetOrderRequest ( )
2023-04-14 08:44:56 +00:00
if len ( q . OrderID ) != 0 {
orderID , err := strconv . ParseInt ( q . OrderID , 10 , 64 )
if err != nil {
return nil , err
}
request . Id ( uint64 ( orderID ) )
}
if len ( q . ClientOrderID ) != 0 {
request . ClientOrderID ( q . ClientOrderID )
2022-02-10 09:48:53 +00:00
}
2023-04-14 08:44:56 +00:00
maxOrder , err := request . Do ( ctx )
2022-02-10 09:48:53 +00:00
if err != nil {
return nil , err
}
return toGlobalOrder ( * maxOrder )
}
2020-10-25 10:26:10 +00:00
func ( e * Exchange ) QueryOpenOrders ( ctx context . Context , symbol string ) ( orders [ ] types . Order , err error ) {
2022-05-26 11:52:38 +00:00
market := toLocalSymbol ( symbol )
walletType := maxapi . WalletTypeSpot
if e . MarginSettings . IsMargin {
walletType = maxapi . WalletTypeMargin
}
2023-05-04 06:37:19 +00:00
maxOrders , err := e . v3client . NewGetWalletOpenOrdersRequest ( walletType ) . Market ( market ) . Do ( ctx )
2020-10-05 10:26:31 +00:00
if err != nil {
2020-10-25 10:26:10 +00:00
return orders , err
2020-10-05 10:26:31 +00:00
}
2020-10-25 10:26:10 +00:00
for _ , maxOrder := range maxOrders {
order , err := toGlobalOrder ( maxOrder )
if err != nil {
return orders , err
}
2020-10-05 10:26:31 +00:00
2020-10-25 10:26:10 +00:00
orders = append ( orders , * order )
}
return orders , err
}
2020-11-05 03:00:51 +00:00
// lastOrderID is not supported on MAX
2022-01-26 15:51:23 +00:00
func ( e * Exchange ) QueryClosedOrders ( ctx context . Context , symbol string , since , until time . Time , lastOrderID uint64 ) ( [ ] types . Order , error ) {
2022-12-23 17:08:28 +00:00
log . Warn ( "!!!MAX EXCHANGE API NOTICE!!! the since/until conditions will not be effected on closed orders query, max exchange does not support time-range-based query" )
2022-04-21 06:11:49 +00:00
return e . queryClosedOrdersByLastOrderID ( ctx , symbol , lastOrderID )
}
func ( e * Exchange ) queryClosedOrdersByLastOrderID ( ctx context . Context , symbol string , lastOrderID uint64 ) ( orders [ ] types . Order , err error ) {
2023-04-12 14:56:23 +00:00
if err := e . closedOrderQueryLimiter . Wait ( ctx ) ; err != nil {
2022-04-21 06:11:49 +00:00
return orders , err
}
2022-05-26 11:52:38 +00:00
market := toLocalSymbol ( symbol )
walletType := maxapi . WalletTypeSpot
if e . MarginSettings . IsMargin {
walletType = maxapi . WalletTypeMargin
}
2023-05-04 06:37:19 +00:00
req := e . v3client . NewGetWalletOrderHistoryRequest ( walletType ) . Market ( market )
2022-04-21 06:11:49 +00:00
if lastOrderID == 0 {
lastOrderID = 1
2022-01-26 15:51:23 +00:00
}
2022-04-21 06:11:49 +00:00
req . FromID ( lastOrderID )
2022-06-17 17:57:34 +00:00
req . Limit ( 1000 )
2022-05-26 11:52:38 +00:00
2022-04-21 06:11:49 +00:00
maxOrders , err := req . Do ( ctx )
if err != nil {
return orders , err
}
for _ , maxOrder := range maxOrders {
order , err2 := toGlobalOrder ( maxOrder )
if err2 != nil {
err = multierr . Append ( err , err2 )
continue
}
orders = append ( orders , * order )
}
2023-07-27 10:35:58 +00:00
if err != nil {
return nil , err
}
return types . SortOrdersAscending ( orders ) , nil
2022-01-26 15:51:23 +00:00
}
2022-05-26 11:52:38 +00:00
func ( e * Exchange ) CancelAllOrders ( ctx context . Context ) ( [ ] types . Order , error ) {
walletType := maxapi . WalletTypeSpot
if e . MarginSettings . IsMargin {
walletType = maxapi . WalletTypeMargin
2022-01-24 15:51:53 +00:00
}
2020-11-05 00:33:57 +00:00
2023-05-04 06:37:19 +00:00
req := e . v3client . NewCancelWalletOrderAllRequest ( walletType )
2023-03-01 08:45:33 +00:00
var orderResponses , err = req . Do ( ctx )
2020-12-29 08:00:03 +00:00
if err != nil {
return nil , err
}
2023-03-01 08:45:33 +00:00
var maxOrders [ ] maxapi . Order
for _ , resp := range orderResponses {
if resp . Error == nil {
maxOrders = append ( maxOrders , resp . Order )
}
}
2020-12-29 08:00:03 +00:00
return toGlobalOrders ( maxOrders )
}
func ( e * Exchange ) CancelOrdersBySymbol ( ctx context . Context , symbol string ) ( [ ] types . Order , error ) {
2022-05-26 11:52:38 +00:00
market := toLocalSymbol ( symbol )
walletType := maxapi . WalletTypeSpot
if e . MarginSettings . IsMargin {
walletType = maxapi . WalletTypeMargin
}
2020-12-29 08:00:03 +00:00
2023-05-04 06:37:19 +00:00
req := e . v3client . NewCancelWalletOrderAllRequest ( walletType )
2022-05-26 11:52:38 +00:00
req . Market ( market )
2023-03-01 08:45:33 +00:00
var orderResponses , err = req . Do ( ctx )
2020-12-29 08:00:03 +00:00
if err != nil {
return nil , err
}
2023-03-01 08:45:33 +00:00
var maxOrders [ ] maxapi . Order
for _ , resp := range orderResponses {
if resp . Error == nil {
maxOrders = append ( maxOrders , resp . Order )
}
}
2020-12-29 08:00:03 +00:00
return toGlobalOrders ( maxOrders )
}
2021-03-22 09:26:06 +00:00
func ( e * Exchange ) CancelOrdersByGroupID ( ctx context . Context , groupID uint32 ) ( [ ] types . Order , error ) {
2022-05-26 11:52:38 +00:00
walletType := maxapi . WalletTypeSpot
if e . MarginSettings . IsMargin {
walletType = maxapi . WalletTypeMargin
}
2023-05-04 06:37:19 +00:00
req := e . v3client . NewCancelWalletOrderAllRequest ( walletType )
2020-12-29 08:00:03 +00:00
req . GroupID ( groupID )
2023-03-01 08:45:33 +00:00
var orderResponses , err = req . Do ( ctx )
2020-12-29 08:00:03 +00:00
if err != nil {
return nil , err
}
2023-03-01 08:45:33 +00:00
var maxOrders [ ] maxapi . Order
for _ , resp := range orderResponses {
if resp . Error == nil {
maxOrders = append ( maxOrders , resp . Order )
}
}
2020-12-29 08:00:03 +00:00
return toGlobalOrders ( maxOrders )
}
2020-10-25 16:26:17 +00:00
func ( e * Exchange ) CancelOrders ( ctx context . Context , orders ... types . Order ) ( err2 error ) {
2022-05-26 11:52:38 +00:00
walletType := maxapi . WalletTypeSpot
if e . MarginSettings . IsMargin {
walletType = maxapi . WalletTypeMargin
}
2021-03-22 09:26:06 +00:00
var groupIDs = make ( map [ uint32 ] struct { } )
2021-01-23 09:15:32 +00:00
var orphanOrders [ ] types . Order
2020-10-25 16:26:17 +00:00
for _ , o := range orders {
2021-01-23 09:17:46 +00:00
if o . GroupID > 0 {
2021-01-23 09:15:32 +00:00
groupIDs [ o . GroupID ] = struct { } { }
} else {
orphanOrders = append ( orphanOrders , o )
}
}
if len ( groupIDs ) > 0 {
for groupID := range groupIDs {
2023-05-04 06:37:19 +00:00
req := e . v3client . NewCancelWalletOrderAllRequest ( walletType )
2021-01-23 09:15:32 +00:00
req . GroupID ( groupID )
if _ , err := req . Do ( ctx ) ; err != nil {
log . WithError ( err ) . Errorf ( "group id order cancel error" )
err2 = err
}
}
}
for _ , o := range orphanOrders {
2023-05-04 06:37:19 +00:00
req := e . v3client . NewCancelOrderRequest ( )
2020-10-25 16:26:17 +00:00
if o . OrderID > 0 {
2022-04-19 11:44:44 +00:00
req . Id ( o . OrderID )
2021-06-06 17:00:01 +00:00
} else if len ( o . ClientOrderID ) > 0 && o . ClientOrderID != types . NoClientOrderID {
2020-10-25 16:26:17 +00:00
req . ClientOrderID ( o . ClientOrderID )
} else {
2020-11-09 08:34:35 +00:00
return fmt . Errorf ( "order id or client order id is not defined, order=%+v" , o )
2020-10-25 16:26:17 +00:00
}
2022-04-19 11:44:44 +00:00
if _ , err := req . Do ( ctx ) ; err != nil {
2020-10-25 16:26:17 +00:00
log . WithError ( err ) . Errorf ( "order cancel error" )
err2 = err
}
}
return err2
}
2022-06-02 03:42:03 +00:00
func ( e * Exchange ) Withdraw ( ctx context . Context , asset string , amount fixedpoint . Value , address string , options * types . WithdrawalOptions ) error {
2021-05-26 15:24:05 +00:00
asset = toLocalCurrency ( asset )
2021-05-11 17:21:04 +00:00
2021-05-11 16:23:13 +00:00
addresses , err := e . client . WithdrawalService . NewGetWithdrawalAddressesRequest ( ) .
2021-05-26 15:24:05 +00:00
Currency ( asset ) .
2021-05-11 16:23:13 +00:00
Do ( ctx )
if err != nil {
return err
}
2021-05-11 17:21:04 +00:00
var whitelistAddress maxapi . WithdrawalAddress
2021-05-11 16:23:13 +00:00
for _ , a := range addresses {
if a . Address == address {
2021-05-11 17:21:04 +00:00
whitelistAddress = a
break
2021-05-11 16:23:13 +00:00
}
}
2021-05-11 17:21:04 +00:00
if whitelistAddress . Address != address {
2021-05-11 16:23:13 +00:00
return fmt . Errorf ( "address %s is not in the whitelist" , address )
}
2021-05-11 17:21:04 +00:00
if whitelistAddress . UUID == "" {
return errors . New ( "address UUID can not be empty" )
}
2021-05-11 16:23:13 +00:00
response , err := e . client . WithdrawalService . NewWithdrawalRequest ( ) .
2021-05-26 15:24:05 +00:00
Currency ( asset ) .
2021-05-11 16:23:13 +00:00
Amount ( amount . Float64 ( ) ) .
AddressUUID ( whitelistAddress . UUID ) .
Do ( ctx )
if err != nil {
return err
}
log . Infof ( "withdrawal request response: %+v" , response )
return nil
}
2022-09-09 10:41:06 +00:00
func ( e * Exchange ) SubmitOrder ( ctx context . Context , order types . SubmitOrder ) ( createdOrder * types . Order , err error ) {
2023-03-21 08:25:16 +00:00
if err := e . submitOrderLimiter . Wait ( ctx ) ; err != nil {
2022-12-15 10:38:57 +00:00
return nil , err
}
2022-06-01 12:34:20 +00:00
walletType := maxapi . WalletTypeSpot
if e . MarginSettings . IsMargin {
walletType = maxapi . WalletTypeMargin
2021-03-22 09:26:06 +00:00
}
2022-09-09 10:41:06 +00:00
o := order
orderType , err := toLocalOrderType ( o . Type )
if err != nil {
return createdOrder , err
}
2021-03-22 09:26:06 +00:00
2022-09-09 10:41:06 +00:00
// case IOC type
if orderType == maxapi . OrderTypeLimit && o . TimeInForce == types . TimeInForceIOC {
orderType = maxapi . OrderTypeIOCLimit
}
2021-06-06 17:00:01 +00:00
2022-09-09 10:41:06 +00:00
var quantityString string
if o . Market . Symbol != "" {
quantityString = o . Market . FormatQuantity ( o . Quantity )
} else {
quantityString = o . Quantity . String ( )
}
2021-03-22 09:26:06 +00:00
2022-09-09 10:41:06 +00:00
clientOrderID := NewClientOrderID ( o . ClientOrderID )
2020-12-03 01:25:47 +00:00
2023-05-04 06:37:19 +00:00
req := e . v3client . NewCreateWalletOrderRequest ( walletType )
2022-09-09 10:41:06 +00:00
req . Market ( toLocalSymbol ( o . Symbol ) ) .
Side ( toLocalSideType ( o . Side ) ) .
Volume ( quantityString ) .
OrderType ( orderType ) .
2023-03-06 08:32:36 +00:00
ClientOrderID ( clientOrderID )
if o . GroupID > 0 {
2023-04-02 16:11:21 +00:00
req . GroupID ( strconv . FormatUint ( uint64 ( o . GroupID % math . MaxInt32 ) , 10 ) )
2023-03-06 08:32:36 +00:00
}
2020-10-25 10:26:10 +00:00
2022-09-09 10:41:06 +00:00
switch o . Type {
case types . OrderTypeStopLimit , types . OrderTypeLimit , types . OrderTypeLimitMaker :
var priceInString string
if o . Market . Symbol != "" {
priceInString = o . Market . FormatPrice ( o . Price )
} else {
priceInString = o . Price . String ( )
2020-10-25 11:18:03 +00:00
}
2022-09-09 10:41:06 +00:00
req . Price ( priceInString )
}
2022-06-01 12:34:20 +00:00
2022-09-09 10:41:06 +00:00
// set stop price field for limit orders
switch o . Type {
case types . OrderTypeStopLimit , types . OrderTypeStopMarket :
var priceInString string
if o . Market . Symbol != "" {
priceInString = o . Market . FormatPrice ( o . StopPrice )
} else {
priceInString = o . StopPrice . String ( )
2020-10-25 10:26:10 +00:00
}
2022-09-09 10:41:06 +00:00
req . StopPrice ( priceInString )
}
2020-10-25 10:26:10 +00:00
2022-09-09 10:41:06 +00:00
retOrder , err := req . Do ( ctx )
if err != nil {
return createdOrder , err
}
2020-10-25 11:18:03 +00:00
2022-09-09 10:41:06 +00:00
if retOrder == nil {
return createdOrder , errors . New ( "returned nil order" )
2020-10-05 10:26:31 +00:00
}
2022-09-09 10:41:06 +00:00
createdOrder , err = toGlobalOrder ( * retOrder )
return createdOrder , err
2020-10-05 10:26:31 +00:00
}
// PlatformFeeCurrency
func ( e * Exchange ) PlatformFeeCurrency ( ) string {
2020-10-14 03:02:10 +00:00
return toGlobalCurrency ( "max" )
2020-10-05 10:26:31 +00:00
}
2021-03-13 12:49:51 +00:00
func ( e * Exchange ) getLaunchDate ( ) ( time . Time , error ) {
// MAX launch date June 21th, 2018
loc , err := time . LoadLocation ( "Asia/Taipei" )
if err != nil {
return time . Time { } , err
}
return time . Date ( 2018 , time . June , 21 , 0 , 0 , 0 , 0 , loc ) , nil
}
2020-10-06 09:28:13 +00:00
func ( e * Exchange ) QueryAccount ( ctx context . Context ) ( * types . Account , error ) {
2023-04-12 14:56:23 +00:00
if err := e . accountQueryLimiter . Wait ( ctx ) ; err != nil {
2021-02-22 05:36:39 +00:00
return nil , err
}
2023-04-11 10:33:51 +00:00
vipLevel , err := e . client . NewGetVipLevelRequest ( ) . Do ( ctx )
2021-03-19 09:06:48 +00:00
if err != nil {
return nil , err
}
// MAX returns the fee rate in the following format:
// "maker_fee": 0.0005 -> 0.05%
// "taker_fee": 0.0015 -> 0.15%
2022-05-27 11:46:28 +00:00
2020-10-18 03:30:37 +00:00
a := & types . Account {
2022-05-27 11:46:28 +00:00
AccountType : types . AccountTypeSpot ,
MarginLevel : fixedpoint . Zero ,
2021-06-08 18:39:23 +00:00
MakerFeeRate : fixedpoint . NewFromFloat ( vipLevel . Current . MakerFee ) , // 0.15% = 0.0015
TakerFeeRate : fixedpoint . NewFromFloat ( vipLevel . Current . TakerFee ) , // 0.15% = 0.0015
2020-10-18 03:30:37 +00:00
}
2022-06-01 11:56:10 +00:00
balances , err := e . QueryAccountBalances ( ctx )
if err != nil {
return nil , err
}
a . UpdateBalances ( balances )
2022-05-27 11:46:28 +00:00
if e . MarginSettings . IsMargin {
a . AccountType = types . AccountTypeMargin
2023-05-04 06:37:19 +00:00
req := e . v3client . NewGetMarginADRatioRequest ( )
2022-05-27 11:46:28 +00:00
adRatio , err := req . Do ( ctx )
if err != nil {
return a , err
}
a . MarginLevel = adRatio . AdRatio
a . TotalAccountValue = adRatio . AssetInUsdt
}
2020-10-18 03:30:37 +00:00
return a , nil
2020-10-06 09:28:13 +00:00
}
2022-06-01 17:27:04 +00:00
func ( e * Exchange ) QueryAccountBalances ( ctx context . Context ) ( types . BalanceMap , error ) {
2023-04-12 14:56:23 +00:00
if err := e . accountQueryLimiter . Wait ( ctx ) ; err != nil {
2022-06-01 17:27:04 +00:00
return nil , err
}
walletType := maxapi . WalletTypeSpot
if e . MarginSettings . IsMargin {
walletType = maxapi . WalletTypeMargin
}
2023-05-04 06:37:19 +00:00
req := e . v3client . NewGetWalletAccountsRequest ( walletType )
2022-06-01 17:27:04 +00:00
accounts , err := req . Do ( ctx )
if err != nil {
return nil , err
}
var balances = make ( types . BalanceMap )
for _ , b := range accounts {
cur := toGlobalCurrency ( b . Currency )
balances [ cur ] = types . Balance {
Currency : cur ,
Available : b . Balance ,
Locked : b . Locked ,
2022-06-01 17:34:14 +00:00
NetAsset : b . Balance . Add ( b . Locked ) . Sub ( b . Debt ) ,
2022-07-08 09:28:07 +00:00
Borrowed : b . Borrowed ,
2022-06-01 17:27:04 +00:00
Interest : b . Interest ,
}
}
return balances , nil
}
2020-10-11 12:08:54 +00:00
func ( e * Exchange ) QueryWithdrawHistory ( ctx context . Context , asset string , since , until time . Time ) ( allWithdraws [ ] types . Withdraw , err error ) {
startTime := since
2021-03-13 12:49:51 +00:00
limit := 1000
2020-10-11 12:08:54 +00:00
txIDs := map [ string ] struct { } { }
2020-10-11 09:35:59 +00:00
2021-03-13 12:49:51 +00:00
emptyTime := time . Time { }
if startTime == emptyTime {
startTime , err = e . getLaunchDate ( )
if err != nil {
return nil , err
}
}
2020-10-11 12:08:54 +00:00
for startTime . Before ( until ) {
2021-03-13 12:49:51 +00:00
// startTime ~ endTime must be in 60 days
2020-10-11 12:08:54 +00:00
endTime := startTime . AddDate ( 0 , 0 , 60 )
if endTime . After ( until ) {
endTime = until
}
log . Infof ( "querying withdraw %s: %s <=> %s" , asset , startTime , endTime )
2023-04-11 10:36:10 +00:00
req := e . client . NewGetWithdrawalHistoryRequest ( )
2020-10-12 09:15:13 +00:00
if len ( asset ) > 0 {
req . Currency ( toLocalCurrency ( asset ) )
}
withdraws , err := req .
2023-04-11 10:52:23 +00:00
From ( startTime ) .
To ( endTime ) .
2021-03-13 12:49:51 +00:00
Limit ( limit ) .
2020-10-11 12:08:54 +00:00
Do ( ctx )
if err != nil {
return allWithdraws , err
}
2021-03-13 12:49:51 +00:00
if len ( withdraws ) == 0 {
startTime = endTime
continue
}
2021-03-11 08:03:07 +00:00
for i := len ( withdraws ) - 1 ; i >= 0 ; i -- {
2021-03-11 03:22:01 +00:00
d := withdraws [ i ]
2020-10-11 12:08:54 +00:00
if _ , ok := txIDs [ d . TxID ] ; ok {
continue
}
// we can convert this later
status := d . State
switch d . State {
case "confirmed" :
status = "completed" // make it compatible with binance
case "submitting" , "submitted" , "accepted" ,
"rejected" , "suspect" , "approved" , "delisted_processing" ,
"processing" , "retryable" , "sent" , "canceled" ,
"failed" , "pending" ,
"kgi_manually_processing" , "kgi_manually_confirmed" , "kgi_possible_failed" ,
"sygna_verifying" :
default :
status = d . State
}
txIDs [ d . TxID ] = struct { } { }
2021-03-13 12:49:51 +00:00
withdraw := types . Withdraw {
2021-03-11 08:03:07 +00:00
Exchange : types . ExchangeMax ,
2023-04-11 10:46:29 +00:00
ApplyTime : types . Time ( d . CreatedAt ) ,
2021-03-11 08:03:07 +00:00
Asset : toGlobalCurrency ( d . Currency ) ,
2022-04-20 05:47:12 +00:00
Amount : d . Amount ,
2021-03-11 08:03:07 +00:00
Address : "" ,
AddressTag : "" ,
TransactionID : d . TxID ,
2022-04-20 05:47:12 +00:00
TransactionFee : d . Fee ,
2021-03-11 03:22:01 +00:00
TransactionFeeCurrency : d . FeeCurrency ,
2020-10-11 12:08:54 +00:00
// WithdrawOrderID: d.WithdrawOrderID,
// Network: d.Network,
Status : status ,
2021-03-13 12:49:51 +00:00
}
allWithdraws = append ( allWithdraws , withdraw )
}
// go next time frame
if len ( withdraws ) < limit {
startTime = endTime
} else {
2021-03-11 07:57:24 +00:00
// its in descending order, so we get the first record
2023-04-11 10:46:29 +00:00
startTime = withdraws [ 0 ] . CreatedAt . Time ( )
2020-10-11 12:08:54 +00:00
}
2020-10-11 09:35:59 +00:00
}
2020-10-11 12:08:54 +00:00
return allWithdraws , nil
}
func ( e * Exchange ) QueryDepositHistory ( ctx context . Context , asset string , since , until time . Time ) ( allDeposits [ ] types . Deposit , err error ) {
startTime := since
2021-03-11 08:03:07 +00:00
limit := 1000
2020-10-11 12:08:54 +00:00
txIDs := map [ string ] struct { } { }
2021-03-14 02:43:19 +00:00
emptyTime := time . Time { }
if startTime == emptyTime {
startTime , err = e . getLaunchDate ( )
if err != nil {
return nil , err
}
}
2020-10-11 12:08:54 +00:00
for startTime . Before ( until ) {
// startTime ~ endTime must be in 90 days
endTime := startTime . AddDate ( 0 , 0 , 60 )
if endTime . After ( until ) {
endTime = until
}
log . Infof ( "querying deposit history %s: %s <=> %s" , asset , startTime , endTime )
2021-03-11 08:03:07 +00:00
2023-04-11 10:36:10 +00:00
req := e . client . NewGetDepositHistoryRequest ( )
2020-10-12 09:15:13 +00:00
if len ( asset ) > 0 {
req . Currency ( toLocalCurrency ( asset ) )
}
deposits , err := req .
2023-04-12 05:09:31 +00:00
From ( startTime ) .
To ( endTime ) .
2021-03-11 08:03:07 +00:00
Limit ( limit ) .
Do ( ctx )
2020-10-11 12:08:54 +00:00
if err != nil {
return nil , err
}
2021-03-11 08:03:07 +00:00
for i := len ( deposits ) - 1 ; i >= 0 ; i -- {
d := deposits [ i ]
2020-10-11 12:08:54 +00:00
if _ , ok := txIDs [ d . TxID ] ; ok {
continue
}
allDeposits = append ( allDeposits , types . Deposit {
2021-03-13 12:49:51 +00:00
Exchange : types . ExchangeMax ,
2023-04-12 05:09:31 +00:00
Time : types . Time ( d . CreatedAt ) ,
2022-04-20 06:01:18 +00:00
Amount : d . Amount ,
2021-03-13 12:49:51 +00:00
Asset : toGlobalCurrency ( d . Currency ) ,
Address : "" , // not supported
AddressTag : "" , // not supported
TransactionID : d . TxID ,
Status : toGlobalDepositStatus ( d . State ) ,
2020-10-11 12:08:54 +00:00
} )
}
2021-03-11 08:03:07 +00:00
if len ( deposits ) < limit {
startTime = endTime
} else {
2023-04-12 05:09:31 +00:00
startTime = time . Time ( deposits [ 0 ] . CreatedAt )
2021-03-11 08:03:07 +00:00
}
2020-10-11 09:35:59 +00:00
}
return allDeposits , err
}
2023-03-14 07:13:34 +00:00
// QueryTrades
// For MAX API spec
// start_time and end_time need to be within 3 days
// without any parameters -> return trades within 24 hours
// give start_time or end_time -> ignore parameter from_id
// give start_time or from_id -> order by time asc
// give end_time -> order by time desc
// limit should b1 1~1000
// For this QueryTrades spec (to be compatible with batch.TradeBatchQuery)
// give LastTradeID -> ignore start_time (but still can filter the end_time)
// without any parameters -> return trades within 24 hours
2020-10-06 10:44:56 +00:00
func ( e * Exchange ) QueryTrades ( ctx context . Context , symbol string , options * types . TradeQueryOptions ) ( trades [ ] types . Trade , err error ) {
2023-03-21 08:26:47 +00:00
if err := e . queryTradeLimiter . Wait ( ctx ) ; err != nil {
2021-02-22 05:36:39 +00:00
return nil , err
}
2022-05-27 11:20:45 +00:00
market := toLocalSymbol ( symbol )
walletType := maxapi . WalletTypeSpot
if e . MarginSettings . IsMargin {
walletType = maxapi . WalletTypeMargin
}
2023-05-04 06:37:19 +00:00
req := e . v3client . NewGetWalletTradesRequest ( walletType )
2022-05-27 11:20:45 +00:00
req . Market ( market )
2020-10-06 10:44:56 +00:00
if options . Limit > 0 {
2022-05-27 11:20:45 +00:00
req . Limit ( uint64 ( options . Limit ) )
2021-02-16 08:32:48 +00:00
} else {
2021-02-22 07:03:15 +00:00
req . Limit ( 1000 )
2020-10-06 10:44:56 +00:00
}
2023-03-14 07:13:34 +00:00
// If we use start_time as parameter, MAX will ignore from_id.
// However, we want to use from_id as main parameter for batch.TradeBatchQuery
2020-10-06 10:44:56 +00:00
if options . LastTradeID > 0 {
2023-03-14 07:13:34 +00:00
// MAX uses inclusive last trade ID
2022-05-27 11:20:45 +00:00
req . From ( options . LastTradeID )
2023-03-14 07:13:34 +00:00
} else {
2023-03-14 10:39:36 +00:00
// option's start_time and end_time need to be within 3 days
// so if the start_time and end_time is over 3 days, we make end_time down to start_time + 3 days
if options . StartTime != nil && options . EndTime != nil {
2023-03-14 07:13:34 +00:00
endTime := * options . EndTime
2023-03-14 10:39:36 +00:00
startTime := * options . StartTime
if endTime . Sub ( startTime ) > 72 * time . Hour {
startTime := * options . StartTime
endTime = startTime . Add ( 72 * time . Hour )
}
req . StartTime ( startTime )
2023-03-14 07:13:34 +00:00
req . EndTime ( endTime )
2023-03-14 10:39:36 +00:00
} else if options . StartTime != nil {
req . StartTime ( * options . StartTime )
} else if options . EndTime != nil {
req . EndTime ( * options . EndTime )
2023-03-14 07:13:34 +00:00
}
2020-10-06 10:44:56 +00:00
}
2022-01-24 15:59:10 +00:00
maxTrades , err := req . Do ( ctx )
2020-10-06 10:44:56 +00:00
if err != nil {
return nil , err
}
2022-01-24 15:59:10 +00:00
for _ , t := range maxTrades {
2023-03-08 09:18:18 +00:00
localTrades , err := toGlobalTradeV3 ( t )
2020-10-06 10:44:56 +00:00
if err != nil {
2022-01-02 04:00:06 +00:00
log . WithError ( err ) . Errorf ( "can not convert trade: %+v" , t )
2020-10-06 10:44:56 +00:00
continue
}
2020-10-10 09:50:49 +00:00
2023-03-08 09:18:18 +00:00
trades = append ( trades , localTrades ... )
2020-10-06 10:44:56 +00:00
}
2022-04-21 06:52:44 +00:00
// ensure everything is sorted ascending
trades = types . SortTradesAscending ( trades )
2020-10-06 10:44:56 +00:00
return trades , nil
}
2021-02-23 08:39:48 +00:00
func ( e * Exchange ) QueryRewards ( ctx context . Context , startTime time . Time ) ( [ ] types . Reward , error ) {
var from = startTime
var emptyTime = time . Time { }
if from == emptyTime {
from = time . Unix ( maxapi . TimestampSince , 0 )
}
var now = time . Now ( )
for {
if from . After ( now ) {
2021-03-10 06:18:01 +00:00
return nil , nil
2021-02-23 08:39:48 +00:00
}
// scan by 30 days
// an user might get most 14 commission records by currency per day
// limit 1000 / 14 = 71 days
to := from . Add ( time . Hour * 24 * 30 )
2022-04-20 08:58:42 +00:00
req := e . client . RewardService . NewGetRewardsRequest ( )
2021-02-23 08:39:48 +00:00
req . From ( from . Unix ( ) )
req . To ( to . Unix ( ) )
req . Limit ( 1000 )
maxRewards , err := req . Do ( ctx )
if err != nil {
return nil , err
}
if len ( maxRewards ) == 0 {
// next page
from = to
continue
}
rewards , err := toGlobalRewards ( maxRewards )
if err != nil {
return nil , err
}
// sort them in the ascending order
2021-02-23 14:53:00 +00:00
sort . Sort ( types . RewardSliceByCreationTime ( rewards ) )
return rewards , nil
2021-02-23 08:39:48 +00:00
}
return nil , errors . New ( "unknown error" )
}
2021-05-05 08:33:15 +00:00
// QueryKLines returns the klines from the MAX exchange API.
// The KLine API of the MAX exchange uses inclusive time range
//
// https://max-api.maicoin.com/api/v2/k?market=btctwd&limit=10&period=1×tamp=1620202440
// The above query will return a kline that starts with 1620202440 (unix timestamp) without endTime.
// We need to calculate the endTime by ourself.
2020-11-06 13:40:48 +00:00
func ( e * Exchange ) QueryKLines ( ctx context . Context , symbol string , interval types . Interval , options types . KLineQueryOptions ) ( [ ] types . KLine , error ) {
2023-04-12 14:56:23 +00:00
if err := e . marketDataLimiter . Wait ( ctx ) ; err != nil {
2021-02-22 05:36:39 +00:00
return nil , err
}
2020-10-10 09:50:49 +00:00
var limit = 5000
if options . Limit > 0 {
// default limit == 500
limit = options . Limit
}
2020-11-06 18:57:50 +00:00
// workaround for the kline query, because MAX does not support query by end time
// so we need to use the given end time and the limit number to calculate the start time
2020-10-31 12:36:58 +00:00
if options . EndTime != nil && options . StartTime == nil {
2021-02-04 21:48:35 +00:00
startTime := options . EndTime . Add ( - time . Duration ( limit ) * interval . Duration ( ) )
2020-10-31 12:36:58 +00:00
options . StartTime = & startTime
}
if options . StartTime == nil {
return nil , errors . New ( "start time can not be empty" )
2020-10-10 09:50:49 +00:00
}
2021-02-22 05:36:39 +00:00
log . Infof ( "querying kline %s %s %+v" , symbol , interval , options )
2020-11-06 13:40:48 +00:00
localKLines , err := e . client . PublicService . KLines ( toLocalSymbol ( symbol ) , string ( interval ) , * options . StartTime , limit )
2020-10-10 09:50:49 +00:00
if err != nil {
return nil , err
}
var kLines [ ] types . KLine
2020-10-31 12:36:58 +00:00
for _ , k := range localKLines {
2021-05-05 08:33:15 +00:00
if options . EndTime != nil && k . StartTime . After ( * options . EndTime ) {
break
}
2020-10-10 09:50:49 +00:00
kLines = append ( kLines , k . KLine ( ) )
}
return kLines , nil
}
2022-02-03 04:55:25 +00:00
var Two = fixedpoint . NewFromInt ( 2 )
func ( e * Exchange ) QueryAveragePrice ( ctx context . Context , symbol string ) ( fixedpoint . Value , error ) {
2020-10-10 09:50:49 +00:00
ticker , err := e . client . PublicService . Ticker ( toLocalSymbol ( symbol ) )
if err != nil {
2022-02-03 04:55:25 +00:00
return fixedpoint . Zero , err
2020-10-10 09:50:49 +00:00
}
2023-04-12 06:56:33 +00:00
return ticker . Sell . Add ( ticker . Buy ) . Div ( Two ) , nil
2020-10-10 09:50:49 +00:00
}
2022-06-01 12:44:24 +00:00
func ( e * Exchange ) RepayMarginAsset ( ctx context . Context , asset string , amount fixedpoint . Value ) error {
2023-05-04 06:37:19 +00:00
req := e . v3client . NewMarginRepayRequest ( )
2022-06-01 12:44:24 +00:00
req . Currency ( toLocalCurrency ( asset ) )
req . Amount ( amount . String ( ) )
resp , err := req . Do ( ctx )
if err != nil {
return err
}
log . Infof ( "margin repay: %v" , resp )
return nil
}
func ( e * Exchange ) BorrowMarginAsset ( ctx context . Context , asset string , amount fixedpoint . Value ) error {
2023-05-04 06:37:19 +00:00
req := e . v3client . NewMarginLoanRequest ( )
2022-06-01 12:44:24 +00:00
req . Currency ( toLocalCurrency ( asset ) )
req . Amount ( amount . String ( ) )
resp , err := req . Do ( ctx )
if err != nil {
return err
}
log . Infof ( "margin borrow: %v" , resp )
return nil
}
func ( e * Exchange ) QueryMarginAssetMaxBorrowable ( ctx context . Context , asset string ) ( amount fixedpoint . Value , err error ) {
2023-05-04 06:37:19 +00:00
req := e . v3client . NewGetMarginBorrowingLimitsRequest ( )
2022-06-01 12:44:24 +00:00
resp , err := req . Do ( ctx )
if err != nil {
return fixedpoint . Zero , err
}
limits := * resp
if limit , ok := limits [ toLocalCurrency ( asset ) ] ; ok {
return limit , nil
}
err = fmt . Errorf ( "borrowing limit of %s not found" , asset )
return amount , err
}
2022-06-02 19:24:34 +00:00
// DefaultFeeRates returns the MAX VIP 0 fee schedule
// See also https://max-vip-zh.maicoin.com/
func ( e * Exchange ) DefaultFeeRates ( ) types . ExchangeFee {
return types . ExchangeFee {
MakerFeeRate : fixedpoint . NewFromFloat ( 0.01 * 0.045 ) , // 0.045%
TakerFeeRate : fixedpoint . NewFromFloat ( 0.01 * 0.150 ) , // 0.15%
}
}
2022-07-26 07:42:34 +00:00
var SupportedIntervals = map [ types . Interval ] int {
2022-10-04 07:15:07 +00:00
types . Interval1m : 1 * 60 ,
types . Interval5m : 5 * 60 ,
types . Interval15m : 15 * 60 ,
types . Interval30m : 30 * 60 ,
types . Interval1h : 60 * 60 ,
types . Interval2h : 60 * 60 * 2 ,
types . Interval4h : 60 * 60 * 4 ,
types . Interval6h : 60 * 60 * 6 ,
types . Interval12h : 60 * 60 * 12 ,
types . Interval1d : 60 * 60 * 24 ,
types . Interval3d : 60 * 60 * 24 * 3 ,
2022-07-26 07:42:34 +00:00
}
func ( e * Exchange ) SupportedInterval ( ) map [ types . Interval ] int {
return SupportedIntervals
}
func ( e * Exchange ) IsSupportedInterval ( interval types . Interval ) bool {
_ , ok := SupportedIntervals [ interval ]
return ok
}
2023-08-08 05:16:11 +00:00
func logResponse ( resp interface { } , err error , req interface { } ) error {
if err != nil {
log . WithError ( err ) . Errorf ( "%T: error %+v" , req , resp )
return err
}
log . Infof ( "%T: response: %+v" , req , resp )
return nil
}