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"
2021-02-18 09:37:49 +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"
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"
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"
"github.com/c9s/bbgo/pkg/util"
2020-10-05 10:26:31 +00:00
)
2020-10-17 02:39:03 +00:00
2022-01-24 15:59:10 +00:00
// closedOrderQueryLimiter is used for the closed orders query rate limit, 1 request per second
2022-01-24 15:18:52 +00:00
var closedOrderQueryLimiter = rate . NewLimiter ( rate . Every ( 1 * time . Second ) , 1 )
2021-05-02 09:46:08 +00:00
var tradeQueryLimiter = rate . NewLimiter ( rate . Every ( 3 * time . Second ) , 1 )
var accountQueryLimiter = rate . NewLimiter ( rate . Every ( 3 * time . Second ) , 1 )
var marketDataLimiter = rate . NewLimiter ( rate . Every ( 2 * time . Second ) , 10 )
2021-02-22 05:36:39 +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 {
2020-10-05 11:01:43 +00:00
client * maxapi . RestClient
2020-10-05 10:26:31 +00:00
key , secret string
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 {
2020-10-05 10:26:31 +00:00
client : client ,
2020-10-05 11:01:43 +00:00
key : key ,
2020-10-05 10:26:31 +00:00
secret : secret ,
}
}
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 ,
Volume : util . MustParseFloat ( ticker . Volume ) ,
Last : util . MustParseFloat ( ticker . Last ) ,
Open : util . MustParseFloat ( ticker . Open ) ,
High : util . MustParseFloat ( ticker . High ) ,
Low : util . MustParseFloat ( ticker . Low ) ,
Buy : util . MustParseFloat ( ticker . Buy ) ,
Sell : util . MustParseFloat ( ticker . Sell ) ,
} , nil
}
2021-02-06 17:35:23 +00:00
func ( e * Exchange ) QueryTickers ( ctx context . Context , symbol ... string ) ( map [ string ] types . Ticker , error ) {
2021-02-23 08:39:48 +00:00
if err := 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 {
2021-02-22 07:01:05 +00:00
maxTickers , err := e . client . PublicService . Tickers ( )
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
}
2021-02-22 07:01:05 +00:00
tickers [ toGlobalSymbol ( k ) ] = types . Ticker {
2021-02-06 17:35:23 +00:00
Time : v . Time ,
Volume : util . MustParseFloat ( v . Volume ) ,
Last : util . MustParseFloat ( v . Last ) ,
Open : util . MustParseFloat ( v . Open ) ,
High : util . MustParseFloat ( v . High ) ,
Low : util . MustParseFloat ( v . Low ) ,
Buy : util . MustParseFloat ( v . Buy ) ,
Sell : util . MustParseFloat ( v . Sell ) ,
}
}
}
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 ) {
2020-10-16 02:14:36 +00:00
log . Info ( "querying market info..." )
2020-10-14 02:53:18 +00:00
remoteMarkets , err := e . client . PublicService . Markets ( )
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 ,
MaxQuantity : 10000.0 ,
StepSize : 1.0 / math . Pow10 ( m . BaseUnitPrecision ) , // make it like 0.0001
MinPrice : 1.0 / math . Pow10 ( m . QuoteUnitPrecision ) , // used in the price formatter
MaxPrice : 10000.0 ,
TickSize : 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 {
return NewStream ( e . key , e . secret )
}
2020-10-25 10:26:10 +00:00
func ( e * Exchange ) QueryOpenOrders ( ctx context . Context , symbol string ) ( orders [ ] types . Order , err error ) {
maxOrders , err := e . client . OrderService . Open ( toLocalSymbol ( symbol ) , maxapi . QueryOrderOptions { } )
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
func ( e * Exchange ) QueryClosedOrders ( ctx context . Context , symbol string , since , until time . Time , lastOrderID uint64 ) ( orders [ ] types . Order , err error ) {
2022-01-24 15:18:52 +00:00
limit := 1000 // max limit = 1000, default 100
2020-11-05 06:12:19 +00:00
orderIDs := make ( map [ uint64 ] struct { } , limit * 2 )
2022-01-24 15:51:53 +00:00
log . Warn ( "since/until condition will not be effected on closed orders query, max exchange does not support time-range-based query, we will start from the first record" )
if lastOrderID > 0 {
log . Warn ( "last order id condition will not be effected on max exchange, max exchange does not support last order id query" )
}
2020-11-05 00:33:57 +00:00
2022-01-24 15:18:52 +00:00
page := 1
for {
2022-01-24 15:45:56 +00:00
if err := closedOrderQueryLimiter . Wait ( ctx ) ; err != nil {
return nil , err
}
2022-01-24 15:18:52 +00:00
log . Infof ( "querying %s closed orders from page %d ~ " , symbol , page )
2020-11-05 00:33:57 +00:00
maxOrders , err := e . client . OrderService . Closed ( toLocalSymbol ( symbol ) , maxapi . QueryOrderOptions {
2022-01-24 15:18:52 +00:00
Limit : limit ,
Page : page ,
2020-11-05 00:33:57 +00:00
} )
if err != nil {
return orders , err
}
if len ( maxOrders ) == 0 {
2022-01-24 15:18:52 +00:00
return orders , err
2020-11-05 00:33:57 +00:00
}
2022-01-24 15:54:58 +00:00
// ensure everything is ascending ordered
sort . Slice ( maxOrders , func ( i , j int ) bool {
return maxOrders [ i ] . CreatedAtMs . Time ( ) . Before ( maxOrders [ j ] . CreatedAtMs . Time ( ) )
} )
2022-01-24 15:18:52 +00:00
log . Infof ( "%d orders" , len ( maxOrders ) )
2020-11-05 00:33:57 +00:00
for _ , maxOrder := range maxOrders {
2022-01-24 15:18:52 +00:00
if maxOrder . CreatedAtMs . Time ( ) . Before ( since ) {
log . Debugf ( "skip orders with creation time before %s, found %s" , since , maxOrder . CreatedAtMs . Time ( ) )
2020-11-05 00:33:57 +00:00
continue
}
2022-01-24 15:18:52 +00:00
if maxOrder . CreatedAtMs . Time ( ) . After ( until ) {
2020-11-05 00:33:57 +00:00
return orders , err
}
order , err := toGlobalOrder ( maxOrder )
if err != nil {
return orders , err
}
if _ , ok := orderIDs [ order . OrderID ] ; ok {
2022-01-24 15:18:52 +00:00
log . Debugf ( "skipping duplicated order: %d" , order . OrderID )
2020-11-05 00:33:57 +00:00
}
orderIDs [ order . OrderID ] = struct { } { }
orders = append ( orders , * order )
2022-01-24 15:18:52 +00:00
log . Infof ( "order %+v" , order )
2020-11-05 00:33:57 +00:00
}
2022-01-24 15:18:52 +00:00
page ++
2020-11-05 00:33:57 +00:00
}
return orders , err
}
2020-12-29 08:00:03 +00:00
func ( e * Exchange ) CancelAllOrders ( ctx context . Context ) ( [ ] types . Order , error ) {
var req = e . client . OrderService . NewOrderCancelAllRequest ( )
var maxOrders , err = req . Do ( ctx )
if err != nil {
return nil , err
}
return toGlobalOrders ( maxOrders )
}
func ( e * Exchange ) CancelOrdersBySymbol ( ctx context . Context , symbol string ) ( [ ] types . Order , error ) {
var req = e . client . OrderService . NewOrderCancelAllRequest ( )
req . Market ( toLocalSymbol ( symbol ) )
var maxOrders , err = req . Do ( ctx )
if err != nil {
return nil , err
}
return toGlobalOrders ( maxOrders )
}
2021-03-22 09:26:06 +00:00
func ( e * Exchange ) CancelOrdersByGroupID ( ctx context . Context , groupID uint32 ) ( [ ] types . Order , error ) {
2020-12-29 08:00:03 +00:00
var req = e . client . OrderService . NewOrderCancelAllRequest ( )
req . GroupID ( groupID )
var maxOrders , err = req . Do ( ctx )
if err != nil {
return nil , err
}
return toGlobalOrders ( maxOrders )
}
2020-10-25 16:26:17 +00:00
func ( e * Exchange ) CancelOrders ( ctx context . Context , orders ... types . Order ) ( err2 error ) {
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 {
var req = e . client . OrderService . NewOrderCancelAllRequest ( )
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 {
2020-10-25 16:26:17 +00:00
var req = e . client . OrderService . NewOrderCancelRequest ( )
if o . OrderID > 0 {
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
}
if err := req . Do ( ctx ) ; err != nil {
log . WithError ( err ) . Errorf ( "order cancel error" )
err2 = err
}
}
return err2
}
2021-03-22 09:26:06 +00:00
func toMaxSubmitOrder ( o types . SubmitOrder ) ( * maxapi . Order , error ) {
symbol := toLocalSymbol ( o . Symbol )
orderType , err := toLocalOrderType ( o . Type )
if err != nil {
return nil , err
}
2020-10-25 10:56:07 +00:00
2022-01-10 17:36:19 +00:00
var quantityString string
if o . Market . Symbol != "" {
quantityString = o . Market . FormatQuantity ( o . Quantity )
} else {
quantityString = strconv . FormatFloat ( o . Quantity , 'f' , - 1 , 64 )
2021-03-22 09:26:06 +00:00
}
maxOrder := maxapi . Order {
Market : symbol ,
Side : toLocalSideType ( o . Side ) ,
OrderType : orderType ,
// Price: priceInString,
2022-01-10 17:36:19 +00:00
Volume : quantityString ,
2021-06-06 17:00:01 +00:00
}
if o . GroupID > 0 {
maxOrder . GroupID = o . GroupID
}
clientOrderID := NewClientOrderID ( o . ClientOrderID )
if len ( clientOrderID ) > 0 {
maxOrder . ClientOID = clientOrderID
2021-03-22 09:26:06 +00:00
}
switch o . Type {
2021-04-11 12:07:05 +00:00
case types . OrderTypeStopLimit , types . OrderTypeLimit , types . OrderTypeLimitMaker , types . OrderTypeIOCLimit :
2022-01-10 17:36:19 +00:00
var priceInString string
if o . Market . Symbol != "" {
priceInString = o . Market . FormatPrice ( o . Price )
} else {
priceInString = strconv . FormatFloat ( o . Price , 'f' , - 1 , 64 )
2021-03-21 03:10:55 +00:00
}
2021-03-22 09:26:06 +00:00
maxOrder . Price = priceInString
}
2021-03-21 03:10:55 +00:00
2021-03-22 09:26:06 +00:00
// set stop price field for limit orders
switch o . Type {
case types . OrderTypeStopLimit , types . OrderTypeStopMarket :
2022-01-10 17:36:19 +00:00
var priceInString string
if o . Market . Symbol != "" {
priceInString = o . Market . FormatPrice ( o . StopPrice )
} else {
priceInString = strconv . FormatFloat ( o . StopPrice , 'f' , - 1 , 64 )
2021-02-18 09:37:49 +00:00
}
2021-03-22 09:26:06 +00:00
maxOrder . StopPrice = priceInString
}
return & maxOrder , nil
}
2021-05-26 15:24:05 +00:00
func ( e * Exchange ) Withdrawal ( ctx context . Context , asset string , amount fixedpoint . Value , address string , options * types . WithdrawalOptions ) error {
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
}
2021-03-22 09:26:06 +00:00
func ( e * Exchange ) SubmitOrders ( ctx context . Context , orders ... types . SubmitOrder ) ( createdOrders types . OrderSlice , err error ) {
2021-06-06 02:49:17 +00:00
if len ( orders ) > 1 && len ( orders ) < 15 {
2021-03-22 09:26:06 +00:00
var ordersBySymbol = map [ string ] [ ] maxapi . Order { }
for _ , o := range orders {
maxOrder , err := toMaxSubmitOrder ( o )
if err != nil {
return nil , err
2021-02-18 09:37:49 +00:00
}
2021-03-22 09:26:06 +00:00
ordersBySymbol [ maxOrder . Market ] = append ( ordersBySymbol [ maxOrder . Market ] , * maxOrder )
2021-02-18 09:37:49 +00:00
}
2021-03-22 09:26:06 +00:00
for symbol , orders := range ordersBySymbol {
req := e . client . OrderService . NewCreateMultiOrderRequest ( )
req . Market ( symbol )
req . AddOrders ( orders ... )
2021-03-21 03:10:55 +00:00
2021-03-22 09:26:06 +00:00
orderResponses , err := req . Do ( ctx )
if err != nil {
return createdOrders , err
2020-12-03 01:25:47 +00:00
}
2021-03-22 09:26:06 +00:00
for _ , resp := range * orderResponses {
if len ( resp . Error ) > 0 {
log . Errorf ( "multi-order submit error: %s" , resp . Error )
continue
}
o , err := toGlobalOrder ( resp . Order )
if err != nil {
return createdOrders , err
}
createdOrders = append ( createdOrders , * o )
}
}
return createdOrders , nil
}
for _ , order := range orders {
maxOrder , err := toMaxSubmitOrder ( order )
if err != nil {
return createdOrders , err
}
req := e . client . OrderService . NewCreateOrderRequest ( ) .
Market ( maxOrder . Market ) .
Side ( maxOrder . Side ) .
2021-06-08 19:07:26 +00:00
Volume ( maxOrder . Volume ) .
2021-06-06 17:00:01 +00:00
OrderType ( string ( maxOrder . OrderType ) )
if len ( maxOrder . ClientOID ) > 0 {
req . ClientOrderID ( maxOrder . ClientOID )
}
2021-03-22 09:26:06 +00:00
if len ( maxOrder . Price ) > 0 {
req . Price ( maxOrder . Price )
2020-12-03 01:25:47 +00:00
}
2021-03-22 09:26:06 +00:00
if len ( maxOrder . StopPrice ) > 0 {
req . StopPrice ( maxOrder . StopPrice )
2020-10-25 10:26:10 +00:00
}
retOrder , err := req . Do ( ctx )
if err != nil {
2020-10-25 11:18:03 +00:00
return createdOrders , err
}
if retOrder == nil {
return createdOrders , errors . New ( "returned nil order" )
2020-10-25 10:26:10 +00:00
}
2020-10-25 11:18:03 +00:00
createdOrder , err := toGlobalOrder ( * retOrder )
if err != nil {
return createdOrders , err
}
createdOrders = append ( createdOrders , * createdOrder )
2020-10-05 10:26:31 +00:00
}
2020-10-25 11:18:03 +00:00
return createdOrders , 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 ) {
2021-02-23 08:39:48 +00:00
if err := accountQueryLimiter . Wait ( ctx ) ; err != nil {
2021-02-22 05:36:39 +00:00
return nil , err
}
2020-10-06 09:28:13 +00:00
userInfo , err := e . client . AccountService . Me ( )
if err != nil {
return nil , err
}
var balances = make ( types . BalanceMap )
for _ , a := range userInfo . Accounts {
balances [ toGlobalCurrency ( a . Currency ) ] = types . Balance {
Currency : toGlobalCurrency ( a . Currency ) ,
2020-11-10 06:19:33 +00:00
Available : fixedpoint . Must ( fixedpoint . NewFromString ( a . Balance ) ) ,
Locked : fixedpoint . Must ( fixedpoint . NewFromString ( a . Locked ) ) ,
2020-10-06 09:28:13 +00:00
}
}
2021-03-19 09:06:48 +00:00
vipLevel , err := e . client . AccountService . VipLevel ( )
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%
2020-10-18 03:30:37 +00:00
a := & types . Account {
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
}
a . UpdateBalances ( balances )
return a , nil
2020-10-06 09:28:13 +00:00
}
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 )
2020-10-12 09:15:13 +00:00
req := e . client . AccountService . NewGetWithdrawalHistoryRequest ( )
if len ( asset ) > 0 {
req . Currency ( toLocalCurrency ( asset ) )
}
withdraws , err := req .
2020-10-11 12:08:54 +00:00
From ( startTime . Unix ( ) ) .
To ( endTime . Unix ( ) ) .
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 ,
2021-05-19 17:32:26 +00:00
ApplyTime : types . Time ( time . Unix ( d . CreatedAt , 0 ) ) ,
2021-03-11 08:03:07 +00:00
Asset : toGlobalCurrency ( d . Currency ) ,
Amount : util . MustParseFloat ( d . Amount ) ,
Address : "" ,
AddressTag : "" ,
TransactionID : d . TxID ,
TransactionFee : util . MustParseFloat ( 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
2021-03-11 08:44:43 +00:00
startTime = time . Unix ( withdraws [ 0 ] . CreatedAt , 0 )
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
2020-10-12 09:15:13 +00:00
req := e . client . AccountService . NewGetDepositHistoryRequest ( )
if len ( asset ) > 0 {
req . Currency ( toLocalCurrency ( asset ) )
}
deposits , err := req .
2020-10-11 12:08:54 +00:00
From ( startTime . Unix ( ) ) .
2021-03-11 08:03:07 +00:00
To ( endTime . Unix ( ) ) .
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 ,
2021-05-19 17:32:26 +00:00
Time : types . Time ( time . Unix ( d . CreatedAt , 0 ) ) ,
2021-03-13 12:49:51 +00:00
Amount : util . MustParseFloat ( d . Amount ) ,
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 {
2021-03-11 08:44:43 +00:00
startTime = time . Unix ( deposits [ 0 ] . CreatedAt , 0 )
2021-03-11 08:03:07 +00:00
}
2020-10-11 09:35:59 +00:00
}
return allDeposits , err
}
2020-10-06 09:28:13 +00:00
func ( e * Exchange ) QueryAccountBalances ( ctx context . Context ) ( types . BalanceMap , error ) {
2021-02-23 08:39:48 +00:00
if err := accountQueryLimiter . Wait ( ctx ) ; err != nil {
2021-02-22 05:36:39 +00:00
return nil , err
}
2020-10-06 09:28:13 +00:00
accounts , err := e . client . AccountService . Accounts ( )
if err != nil {
return nil , err
}
var balances = make ( types . BalanceMap )
for _ , a := range accounts {
balances [ toGlobalCurrency ( a . Currency ) ] = types . Balance {
Currency : toGlobalCurrency ( a . Currency ) ,
2020-11-10 06:19:33 +00:00
Available : fixedpoint . Must ( fixedpoint . NewFromString ( a . Balance ) ) ,
Locked : fixedpoint . Must ( fixedpoint . NewFromString ( a . Locked ) ) ,
2020-10-06 09:28:13 +00:00
}
}
return balances , nil
}
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 ) {
2021-02-23 08:39:48 +00:00
if err := tradeQueryLimiter . Wait ( ctx ) ; err != nil {
2021-02-22 05:36:39 +00:00
return nil , err
}
2020-10-06 10:44:56 +00:00
req := e . client . TradeService . NewPrivateTradeRequest ( )
2020-10-10 09:50:49 +00:00
req . Market ( toLocalSymbol ( symbol ) )
2020-10-06 10:44:56 +00:00
if options . Limit > 0 {
req . Limit ( 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
}
2021-02-18 09:37:49 +00:00
// MAX uses exclusive last trade ID
2022-01-12 07:33:04 +00:00
// the timestamp parameter is used for reverse order, we can't use it.
2020-10-06 10:44:56 +00:00
if options . LastTradeID > 0 {
2021-12-23 05:15:27 +00:00
req . From ( int64 ( options . LastTradeID ) )
2020-10-06 10:44:56 +00:00
}
2020-10-10 09:50:49 +00:00
// make it compatible with binance, we need the last trade id for the next page.
req . OrderBy ( "asc" )
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
// ensure everything is sorted ascending
sort . Slice ( maxTrades , func ( i , j int ) bool {
return maxTrades [ i ] . CreatedAtMilliSeconds < maxTrades [ j ] . CreatedAtMilliSeconds
} )
for _ , t := range maxTrades {
2020-10-25 10:26:10 +00:00
localTrade , err := toGlobalTrade ( 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
2020-10-06 10:44:56 +00:00
trades = append ( trades , * localTrade )
}
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 )
req := e . client . RewardService . NewRewardsRequest ( )
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 ) {
2021-02-23 08:39:48 +00:00
if err := 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
}
func ( e * Exchange ) QueryAveragePrice ( ctx context . Context , symbol string ) ( float64 , error ) {
ticker , err := e . client . PublicService . Ticker ( toLocalSymbol ( symbol ) )
if err != nil {
return 0 , err
}
return ( util . MustParseFloat ( ticker . Sell ) + util . MustParseFloat ( ticker . Buy ) ) / 2 , nil
}